mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-17 04:41:21 +00:00
Merge branch 'main' into feat/generic-n-extruders
This commit is contained in:
@@ -20,6 +20,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Widgets/FilamentLoad.hpp
|
||||
GUI/AboutDialog.cpp
|
||||
GUI/AboutDialog.hpp
|
||||
GUI/AnimatedTexture.cpp
|
||||
GUI/AnimatedTexture.hpp
|
||||
GUI/AmsMappingPopup.cpp
|
||||
GUI/AmsMappingPopupUpdate.cpp
|
||||
GUI/AmsMappingPopup.hpp
|
||||
@@ -127,6 +129,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Shortcuts.hpp
|
||||
GUI/SpeedDialDialog.cpp
|
||||
GUI/SpeedDialDialog.hpp
|
||||
GUI/ColorPickerDialog.cpp
|
||||
GUI/ColorPickerDialog.hpp
|
||||
GUI/ActionRegistry.cpp
|
||||
GUI/ActionRegistry.hpp
|
||||
GUI/NativeCommands.cpp
|
||||
@@ -174,6 +178,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Gizmos/GLGizmoAssembly.hpp
|
||||
GUI/Gizmos/GLGizmoBase.cpp
|
||||
GUI/Gizmos/GLGizmoBase.hpp
|
||||
GUI/Gizmos/GLGizmoAlignment.cpp
|
||||
GUI/Gizmos/GLGizmoAlignment.hpp
|
||||
GUI/Gizmos/GLGizmoBrimEars.cpp
|
||||
GUI/Gizmos/GLGizmoBrimEars.hpp
|
||||
GUI/Gizmos/GLGizmoCut.cpp
|
||||
|
||||
@@ -297,6 +297,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
|
||||
, printable(true)
|
||||
, visible(true)
|
||||
, is_active(true)
|
||||
, on_hidden_plate(false)
|
||||
, zoom_to_volumes(true)
|
||||
, shader_outside_printer_detection_enabled(false)
|
||||
, is_outside(false)
|
||||
@@ -824,6 +825,8 @@ bool GLVolume::is_below_printbed() const
|
||||
|
||||
void GLVolume::render_sinking_contours()
|
||||
{
|
||||
if (!is_active)
|
||||
return;
|
||||
m_sinking_contours.render();
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ public:
|
||||
bool visible : 1;
|
||||
// Whether or not this volume is active for rendering
|
||||
bool is_active : 1;
|
||||
// Whether or not is_active was cleared because the other plates are hidden
|
||||
bool on_hidden_plate : 1;
|
||||
// Whether or not to use this volume when applying zoom_to_volumes()
|
||||
bool zoom_to_volumes : 1;
|
||||
// Wheter or not this volume is enabled for outside print volume detection in shader.
|
||||
|
||||
@@ -513,7 +513,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent)
|
||||
m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
|
||||
|
||||
Label* time_unit_label = new Label(parent, "H");
|
||||
Label* time_unit_label = new Label(parent, _L_CONTEXT("H", "Hours unit"));
|
||||
time_unit_label->SetForegroundColour(*wxBLACK);
|
||||
time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1));
|
||||
time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#include "AMSMaterialsSetting.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <cstdint>
|
||||
#include "ExtrusionCalibration.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -36,11 +41,9 @@
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
@@ -1963,11 +1966,6 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
sizer_custom->Add(m_ts_stbitmap_custom, 0, wxEXPAND, 0);
|
||||
m_custom_cp->Layout();
|
||||
|
||||
m_clrData = new wxColourData();
|
||||
m_clrData->SetChooseFull(true);
|
||||
m_clrData->SetChooseAlpha(false);
|
||||
|
||||
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
@@ -1991,46 +1989,34 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
|
||||
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
{
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
const bool was_shown = IsShown();
|
||||
std::optional<wxColour> picker_color;
|
||||
{
|
||||
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
|
||||
// Snapshot the custom control's anchor before releasing the transient popup grab.
|
||||
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
|
||||
Dismiss();
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
|
||||
}
|
||||
} // Destroy the modal before restoring the first-level popup and its focus/grab.
|
||||
if (!picker_color) {
|
||||
if (was_shown) Popup();
|
||||
return;
|
||||
}
|
||||
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
|
||||
wxColour picker_color;
|
||||
|
||||
if (clr_dialog->ShowModal() == wxID_OK) {
|
||||
m_clrData = &(clr_dialog->GetColourData());
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
|
||||
picker_color = wxColour(
|
||||
m_clrData->GetColour().Red(),
|
||||
m_clrData->GetColour().Green(),
|
||||
m_clrData->GetColour().Blue(),
|
||||
255
|
||||
);
|
||||
|
||||
if (picker_color.Alpha() == 0) {
|
||||
m_ts_stbitmap_custom->Show();
|
||||
}
|
||||
else {
|
||||
m_ts_stbitmap_custom->Hide();
|
||||
m_custom_cp->SetBackgroundColor(picker_color);
|
||||
}
|
||||
|
||||
set_def_colour(picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((picker_color.Red() & 0xff) << 24) + ((picker_color.Green() & 0xff) << 16) + ((picker_color.Blue() & 0xff) << 8) + (picker_color.Alpha() & 0xff);
|
||||
evt.SetInt(g_col);
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
set_def_colour(*picker_color);
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
|
||||
(std::uint32_t(picker_color->Green()) << 16) |
|
||||
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
|
||||
evt.SetInt(static_cast<int>(packed));
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
|
||||
@@ -85,7 +85,6 @@ public:
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
wxColourData* m_clrData;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#include "AnimatedTexture.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <wx/animdecod.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/gifdecod.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<unsigned char> read_file_bytes(const std::string &path)
|
||||
{
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool has_gif_signature(const std::vector<unsigned char> &bytes)
|
||||
{
|
||||
return bytes.size() > 6 && bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F';
|
||||
}
|
||||
|
||||
// wxImage keeps colour and alpha in two separate buffers, and a file with no alpha at all has none;
|
||||
// OpenGL wants the four channels interleaved.
|
||||
std::vector<unsigned char> image_to_rgba(const wxImage &image)
|
||||
{
|
||||
const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight());
|
||||
const unsigned char *rgb = image.GetData();
|
||||
const unsigned char *a = image.HasAlpha() ? image.GetAlpha() : nullptr;
|
||||
|
||||
std::vector<unsigned char> out(n * 4);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
out[i * 4 + 0] = rgb[i * 3 + 0];
|
||||
out[i * 4 + 1] = rgb[i * 3 + 1];
|
||||
out[i * 4 + 2] = rgb[i * 3 + 2];
|
||||
out[i * 4 + 3] = a != nullptr ? a[i] : 255;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// One decoded frame on its way to the GPU: the whole animation's area, already composited.
|
||||
struct DecodedFrame
|
||||
{
|
||||
std::vector<unsigned char> rgba;
|
||||
double delay; // seconds
|
||||
};
|
||||
|
||||
// Every frame of a GIF, composited onto the animation's own canvas.
|
||||
//
|
||||
// A GIF frame is a sub-rectangle that is drawn over what came before it, and the one before says how
|
||||
// to clean up after itself (leave it, erase it, put back what it covered). Without replaying that, an
|
||||
// animation that only re-sends the pixels that changed - which is most of them - draws as a flicker of
|
||||
// disconnected patches. wxGIFDecoder hands over the raw palette indices, which is what makes replaying
|
||||
// it here straightforward: a transparent index means "keep what is underneath", nothing else does.
|
||||
bool decode_gif(const std::vector<unsigned char> &bytes, int &width, int &height, std::vector<DecodedFrame> &out)
|
||||
{
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
wxGIFDecoder decoder;
|
||||
|
||||
// A truncated file still decodes everything up to the cut, which is worth showing.
|
||||
const wxGIFErrorCode err = decoder.LoadGIF(stream);
|
||||
if ((err != wxGIF_OK && err != wxGIF_TRUNCATED) || decoder.GetFrameCount() == 0)
|
||||
return false;
|
||||
|
||||
const wxSize size = decoder.GetAnimationSize();
|
||||
width = size.GetWidth();
|
||||
height = size.GetHeight();
|
||||
if (width <= 0 || height <= 0)
|
||||
return false;
|
||||
|
||||
std::vector<unsigned char> canvas(size_t(width) * size_t(height) * 4, 0);
|
||||
std::vector<unsigned char> saved;
|
||||
|
||||
for (unsigned int i = 0; i < decoder.GetFrameCount(); ++i) {
|
||||
const wxSize frame_size = decoder.GetFrameSize(i);
|
||||
const wxPoint frame_pos = decoder.GetFramePosition(i);
|
||||
const wxAnimationDisposal disposal = decoder.GetDisposalMethod(i);
|
||||
const unsigned char *indices = decoder.GetData(i);
|
||||
const unsigned char *palette = decoder.GetPalette(i);
|
||||
const int transparent = decoder.GetTransparentColourIndex(i);
|
||||
|
||||
if (disposal == wxANIM_TOPREVIOUS)
|
||||
saved = canvas;
|
||||
|
||||
if (indices != nullptr && palette != nullptr)
|
||||
for (int y = 0; y < frame_size.GetHeight(); ++y) {
|
||||
const int cy = frame_pos.y + y;
|
||||
if (cy < 0 || cy >= height)
|
||||
continue;
|
||||
for (int x = 0; x < frame_size.GetWidth(); ++x) {
|
||||
const int cx = frame_pos.x + x;
|
||||
if (cx < 0 || cx >= width)
|
||||
continue;
|
||||
const unsigned char index = indices[size_t(y) * size_t(frame_size.GetWidth()) + size_t(x)];
|
||||
if (int(index) == transparent)
|
||||
continue;
|
||||
unsigned char *dst = &canvas[(size_t(cy) * size_t(width) + size_t(cx)) * 4];
|
||||
dst[0] = palette[3 * size_t(index) + 0];
|
||||
dst[1] = palette[3 * size_t(index) + 1];
|
||||
dst[2] = palette[3 * size_t(index) + 2];
|
||||
dst[3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// Browsers treat 0 and 1 hundredths of a second as "as fast as the file can mean", and settle
|
||||
// on 100 ms; an animation authored against that convention plays at the intended speed only if
|
||||
// this does the same.
|
||||
const long delay_ms = decoder.GetDelay(i);
|
||||
out.push_back({ canvas, (delay_ms <= 10 ? 100.0 : double(delay_ms)) / 1000.0 });
|
||||
|
||||
if (disposal == wxANIM_TOBACKGROUND) {
|
||||
for (int y = 0; y < frame_size.GetHeight(); ++y) {
|
||||
const int cy = frame_pos.y + y;
|
||||
if (cy < 0 || cy >= height)
|
||||
continue;
|
||||
const int x0 = std::max(0, frame_pos.x);
|
||||
const int x1 = std::min(width, frame_pos.x + frame_size.GetWidth());
|
||||
if (x1 > x0)
|
||||
std::fill_n(&canvas[(size_t(cy) * size_t(width) + size_t(x0)) * 4], size_t(x1 - x0) * 4, 0);
|
||||
}
|
||||
} else if (disposal == wxANIM_TOPREVIOUS) {
|
||||
canvas = saved;
|
||||
}
|
||||
}
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool AnimatedTexture::load_from_file(const std::string &path)
|
||||
{
|
||||
reset();
|
||||
|
||||
boost::system::error_code ec;
|
||||
if (!boost::filesystem::is_regular_file(path, ec))
|
||||
return false;
|
||||
|
||||
const std::vector<unsigned char> bytes = read_file_bytes(path);
|
||||
if (bytes.empty())
|
||||
return false;
|
||||
|
||||
std::vector<DecodedFrame> frames;
|
||||
int width = 0, height = 0;
|
||||
|
||||
if (has_gif_signature(bytes)) {
|
||||
if (!decode_gif(bytes, width, height, frames)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode GIF " << path;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
wxLogNull no_log; // a format wxWidgets cannot read is handled here, not in a dialog
|
||||
wxImage image;
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
if (!image.LoadFile(stream, wxBITMAP_TYPE_ANY) || !image.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode image " << path;
|
||||
return false;
|
||||
}
|
||||
width = image.GetWidth();
|
||||
height = image.GetHeight();
|
||||
frames.push_back({ image_to_rgba(image), 0.0 });
|
||||
}
|
||||
|
||||
m_width = width;
|
||||
m_height = height;
|
||||
m_frames.reserve(frames.size());
|
||||
for (DecodedFrame &frame : frames) {
|
||||
auto texture = std::make_unique<GLTexture>();
|
||||
// Mipmapped: a hint is drawn at whatever width the panel has room for, which is usually
|
||||
// smaller than the source, and an unfiltered downscale of a diagram is all aliasing.
|
||||
if (!texture->load_from_raw_data(std::move(frame.rgba), (unsigned int) width, (unsigned int) height, false,
|
||||
/* use_mipmaps */ true)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot upload " << path;
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
m_frames.push_back({ std::move(texture), m_duration, frame.delay });
|
||||
m_duration += frame.delay;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AnimatedTexture::reset()
|
||||
{
|
||||
m_frames.clear();
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
m_duration = 0.;
|
||||
}
|
||||
|
||||
unsigned int AnimatedTexture::texture_id(double time) const
|
||||
{
|
||||
if (m_frames.empty())
|
||||
return 0;
|
||||
if (m_frames.size() == 1 || m_duration <= 0.)
|
||||
return m_frames.front().texture->get_id();
|
||||
|
||||
const double t = time - m_duration * std::floor(time / m_duration);
|
||||
// Frames are few; a linear walk beats keeping an index around and getting it stale.
|
||||
size_t i = 0;
|
||||
while (i + 1 < m_frames.size() && t >= m_frames[i].start + m_frames[i].delay)
|
||||
++i;
|
||||
return m_frames[i].texture->get_id();
|
||||
}
|
||||
|
||||
const AnimatedTexture *AnimatedTextureCache::get(const std::string &path)
|
||||
{
|
||||
const auto it = m_items.find(path);
|
||||
if (it != m_items.end())
|
||||
return it->second.get();
|
||||
|
||||
auto texture = std::make_unique<AnimatedTexture>();
|
||||
if (!texture->load_from_file(path))
|
||||
texture.reset(); // remembered as a failure, so the file is not re-read every frame
|
||||
|
||||
const AnimatedTexture *result = texture.get();
|
||||
m_items.emplace(path, std::move(texture));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef slic3r_AnimatedTexture_hpp_
|
||||
#define slic3r_AnimatedTexture_hpp_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// An illustration for the UI to draw: one still image, or the frames of an animated GIF together with
|
||||
// the delays the file asks for. Decoded on the CPU once and uploaded as one GL texture per frame.
|
||||
//
|
||||
// A texture per frame rather than an atlas: these are small hints a few hundred pixels across and a
|
||||
// handful of frames long, so the extra textures cost little, while drawing a frame stays a plain
|
||||
// ImGui::Image with no UV arithmetic and no dependence on the maximum texture size.
|
||||
//
|
||||
// load_from_file() uploads to the GPU, so it needs a current GL context - call it from rendering code.
|
||||
class AnimatedTexture
|
||||
{
|
||||
public:
|
||||
AnimatedTexture() = default;
|
||||
AnimatedTexture(AnimatedTexture &&) = default;
|
||||
AnimatedTexture &operator=(AnimatedTexture &&) = default;
|
||||
AnimatedTexture(const AnimatedTexture &) = delete;
|
||||
AnimatedTexture &operator=(const AnimatedTexture &) = delete;
|
||||
|
||||
// PNG, JPG and BMP through wxImage; GIF, animated or not, through wxGIFDecoder. False, and nothing
|
||||
// loaded, if the file is missing or cannot be decoded.
|
||||
bool load_from_file(const std::string &path);
|
||||
void reset();
|
||||
|
||||
bool is_valid() const { return !m_frames.empty(); }
|
||||
bool is_animated() const { return m_frames.size() > 1; }
|
||||
int width() const { return m_width; }
|
||||
int height() const { return m_height; }
|
||||
// Length of one loop, in seconds; 0 for a still image.
|
||||
double duration() const { return m_duration; }
|
||||
|
||||
// The texture to draw `time` seconds into the loop, which wraps. 0 when nothing is loaded.
|
||||
unsigned int texture_id(double time) const;
|
||||
|
||||
private:
|
||||
struct Frame
|
||||
{
|
||||
std::unique_ptr<GLTexture> texture;
|
||||
double start{0.}; // seconds into the loop this frame appears at
|
||||
double delay{0.}; // seconds it stays up
|
||||
};
|
||||
|
||||
std::vector<Frame> m_frames;
|
||||
int m_width{0};
|
||||
int m_height{0};
|
||||
double m_duration{0.};
|
||||
};
|
||||
|
||||
// The illustrations a panel has shown at least once, keyed by absolute path, decoded and uploaded on
|
||||
// first use and kept until clear() - which has to run while the GL context is still current.
|
||||
class AnimatedTextureCache
|
||||
{
|
||||
public:
|
||||
// Null when the file is missing or cannot be decoded. The failure is remembered too, so a tooltip
|
||||
// whose illustration has not been drawn yet costs one lookup, not one decode attempt per frame.
|
||||
const AnimatedTexture *get(const std::string &path);
|
||||
void clear() { m_items.clear(); }
|
||||
|
||||
private:
|
||||
std::map<std::string, std::unique_ptr<AnimatedTexture>> m_items; // null value: tried once, failed
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_AnimatedTexture_hpp_
|
||||
@@ -974,12 +974,12 @@ void AuxiliaryPanel::on_import_file(wxCommandEvent &event)
|
||||
|
||||
if (file_model == s_default_folders[MODEL_PICTURE]) {
|
||||
//wildcard = wxT("JPEG files (*.jpeg)|*.jpeg|BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg");
|
||||
wildcard = wxT("files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp");
|
||||
wildcard = _L("Image files") + " (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
|
||||
}
|
||||
|
||||
if (file_model == s_default_folders[OTHERS]) { wildcard = wxT("TXT files (*.txt)|*.txt"); }
|
||||
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = wxT("EXCEL files (*.xls)|*.xls|EXCEL files (*.xlsx)|*.xlsx|PDF files (*.pdf)|*.pdf"); }
|
||||
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = wxT("PDF files (*.pdf)|*.pdf"); }
|
||||
if (file_model == s_default_folders[OTHERS]) { wildcard = _L("TXT files") + " (*.txt)|*.txt"; }
|
||||
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = _L("EXCEL files") + " (*.xls)|*.xls|" + _L("EXCEL files") + " (*.xlsx)|*.xlsx|" + _L("PDF files") + " (*.pdf)|*.pdf"; }
|
||||
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = _L("PDF files") + " (*.pdf)|*.pdf"; }
|
||||
|
||||
wxFileDialog dialog(this, _L("Choose files"), wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
|
||||
if (dialog.ShowModal() == wxID_OK) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -57,6 +58,7 @@
|
||||
#include "libslic3r_version.h"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/Utils/PrintHost.hpp"
|
||||
@@ -122,7 +124,7 @@ std::pair<std::string, std::vector<size_t>> SlicingProcessCompletedEvent::format
|
||||
} catch (std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown C++ exception.";
|
||||
error = _u8L("Unknown C++ exception.");
|
||||
}
|
||||
return std::make_pair(std::move(error), std::vector<size_t>{monospace});
|
||||
}
|
||||
@@ -781,6 +783,7 @@ void BackgroundSlicingProcess::schedule_export(const std::string& path, bool exp
|
||||
this->invalidate_step(bspsGCodeFinalize);
|
||||
m_export_path = path;
|
||||
m_export_path_on_removable_media = export_path_on_removable_media;
|
||||
m_export_statistics = GUI::wxGetApp().app_config->get_bool("export_statistics");
|
||||
}
|
||||
|
||||
void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
|
||||
@@ -906,6 +909,8 @@ void BackgroundSlicingProcess::finalize_gcode()
|
||||
BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int) copy_ret_val << ") durring copy_file() to " << export_path << ".";
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
m_print->set_status(100, GUI::format(_L("G-code file exported to %1%"), export_path));
|
||||
}
|
||||
@@ -953,6 +958,8 @@ void BackgroundSlicingProcess::export_gcode()
|
||||
// throw Slic3r::ExportError(_utf8(L("Unknown error when exporting G-code.")));
|
||||
break;
|
||||
}
|
||||
if (m_export_statistics)
|
||||
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
|
||||
|
||||
// BBS
|
||||
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
|
||||
|
||||
@@ -258,6 +258,8 @@ private:
|
||||
// but once set, it cannot be re-set.
|
||||
std::string m_export_path;
|
||||
bool m_export_path_on_removable_media = false;
|
||||
// Read from the preferences when the export is scheduled, as the background thread may not read them.
|
||||
bool m_export_statistics = false;
|
||||
// Print host upload job to schedule after slicing is complete, used by schedule_upload(),
|
||||
// empty by default (ie. no upload to schedule)
|
||||
PrintHostJob m_upload_job;
|
||||
|
||||
@@ -931,7 +931,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
});
|
||||
|
||||
feat_dropdown("plane", "design_plane", _L("Datum / Curve (plane / axis / coord sys / helix / project)"), {
|
||||
{"design_plane", _L("Plane"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
|
||||
{"design_plane", _L_CONTEXT("Plane", "Design"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
|
||||
[this] {
|
||||
populate_plane_choices(m_plane_base);
|
||||
reset_plane_refs();
|
||||
@@ -1011,7 +1011,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
open_tool(Tool::Transform);
|
||||
}, SHIFT('Y')},
|
||||
{"design_mirror", _L("Mirror"), _L("Reflect a body about a plane"),
|
||||
{"design_mirror", _L_CONTEXT("Mirror", "Design"), _L("Reflect a body about a plane"),
|
||||
[this] {
|
||||
if (m_doc.bodies.empty()) {
|
||||
set_status(StatusKind::Error, _L("Mirror needs a body — add or import one first"));
|
||||
@@ -1143,7 +1143,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// Hole / Thread — drilling into a solid (both face-aware)
|
||||
feat_dropdown("hole", "design_hole", _L("Hole / thread"), {
|
||||
{"design_hole", _L("Hole"), _L("Drill a hole, centered on a picked face or placed on a plane"),
|
||||
{"design_hole", _L_CONTEXT("Hole", "Design"), _L("Drill a hole, centered on a picked face or placed on a plane"),
|
||||
[this] {
|
||||
// #2: drill on the picked solid face, centred on it (origin = face centroid,
|
||||
// normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then
|
||||
@@ -1344,7 +1344,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
_L("Click 2 points or a line / circle / arc to place a dimension"));
|
||||
// (separator dropped: the group it divided is now reached from the offer)
|
||||
dropdown("design_line", _L("Line / polyline"), {
|
||||
{"design_line", DesignSketchTool::Mode::Line, _L("Line"), _L("Click start, then end — then set the exact length")},
|
||||
{"design_line", DesignSketchTool::Mode::Line, _L_CONTEXT("Line", "Design"), _L("Click start, then end — then set the exact length")},
|
||||
{"design_polyline", DesignSketchTool::Mode::Polyline, _L("Polyline"), _L("Click points; click the first point to close the loop, right-click or Enter to end the chain")} });
|
||||
dropdown("design_rect", _L("Rectangle"), {
|
||||
{"design_rect", DesignSketchTool::Mode::CornerRect, _L("Corner rectangle"), _L("Click two opposite corners")},
|
||||
@@ -1407,7 +1407,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
{"design_chamfer", DesignSketchTool::Mode::Chamfer, _L("Chamfer"), _L("Pick two lines, then drag the arrow or click the distance to set it")} });
|
||||
skbtn("design_offset", DesignSketchTool::Mode::Offset, _L("Offset"),
|
||||
_L("Pick an entity, then drag the arrow or click the distance; click empty to apply"));
|
||||
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L("Mirror"),
|
||||
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L_CONTEXT("Mirror", "Design"),
|
||||
_L("Pick a mirror-axis line, then the entities to mirror; click empty to apply"));
|
||||
// Trim / Extend scissors — standalone sketch tools (NOT inside Constrain): click a
|
||||
// segment to cut it back to / out to its nearest intersection. One cut per click.
|
||||
@@ -1438,9 +1438,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
auto arm_polygon = [this, select_tool] {
|
||||
push_polygon_params();
|
||||
select_tool(DesignSketchTool::Mode::Polygon,
|
||||
wxString::Format(_L("Click center then a vertex — %d sides, %s"),
|
||||
m_poly_sides,
|
||||
m_poly_circumscribed ? _L("circumscribed") : _L("inscribed")));
|
||||
wxString::Format(m_poly_circumscribed
|
||||
? _L_PLURAL("Click center then a vertex — %d side, circumscribed",
|
||||
"Click center then a vertex — %d sides, circumscribed", m_poly_sides)
|
||||
: _L_PLURAL("Click center then a vertex — %d side, inscribed",
|
||||
"Click center then a vertex — %d sides, inscribed", m_poly_sides),
|
||||
m_poly_sides));
|
||||
};
|
||||
for (int n : {3, 4, 5, 6, 8, 12})
|
||||
m_verb_actions["btn:poly#" + std::to_string(n)] =
|
||||
@@ -1998,7 +2001,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
hform->Add(m_hole_target_label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
m_box_hole = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_hole->Add(card_header(m_cards, "design_hole", _L("Hole"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_hole->Add(card_header(m_cards, "design_hole", _L_CONTEXT("Hole", "Design"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_hole->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
m_box_hole->Add(hform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
cards->Add(m_box_hole, 0, wxEXPAND);
|
||||
@@ -2022,7 +2025,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_thread_std->Append(s.name);
|
||||
m_thread_std->SetSelection(0);
|
||||
m_thread_std->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { apply_thread_standard(); });
|
||||
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Standard", "Thread standard")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
tform->Add(m_thread_std, 0, wxEXPAND);
|
||||
|
||||
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
|
||||
@@ -2233,7 +2236,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
m_cut_plane = make_combo(m_cards);
|
||||
m_cut_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
|
||||
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
cform->Add(m_cut_plane, 0, wxEXPAND);
|
||||
|
||||
m_cut_offset = make_spin(m_cards, 0.0, -10000.0, 10000.0);
|
||||
@@ -2258,7 +2261,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// --- Plane (datum/reference plane: offset + tilt from a base plane; no solid) ---
|
||||
m_box_plane = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_plane->Add(card_header(m_cards, "design_plane", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(card_header(m_cards, "design_plane", _L_CONTEXT("Plane", "Design"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_plane->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
// Plane type chooses which inputs matter (Onshape/Fusion parity):
|
||||
@@ -2570,7 +2573,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
|
||||
// --- Mirror (reflect a body about a plane) ---
|
||||
m_box_mirror = new wxBoxSizer(wxVERTICAL);
|
||||
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L("Mirror"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L_CONTEXT("Mirror", "Design"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
m_box_mirror->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
|
||||
{
|
||||
auto* mform = two_col_form();
|
||||
@@ -2748,7 +2751,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_helix_plane = make_combo(m_cards);
|
||||
populate_plane_choices(m_helix_plane);
|
||||
m_helix_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
|
||||
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
|
||||
hform->Add(m_helix_plane, 0, wxEXPAND);
|
||||
m_helix_radius = make_spin(m_cards, 10.0, 0.01, 10000.0);
|
||||
m_helix_radius->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); });
|
||||
@@ -3556,7 +3559,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_doc.recompute();
|
||||
set_status(StatusKind::Info, cons.empty()
|
||||
? _L("Sketch created — select it, then right-click to Extrude")
|
||||
: wxString::Format(_L("Sketch created (%zu driving dims) — select it, then right-click to Extrude"),
|
||||
: wxString::Format(_L_PLURAL("Sketch created (%zu driving dim) — select it, then right-click to Extrude",
|
||||
"Sketch created (%zu driving dims) — select it, then right-click to Extrude",
|
||||
unsigned(cons.size())),
|
||||
cons.size()));
|
||||
refresh_tree();
|
||||
sync_sketch_display(); // keep the just-committed sketch visible as a face
|
||||
@@ -4585,8 +4590,8 @@ void DesignPanel::on_shape_changed()
|
||||
|
||||
void DesignPanel::set_status_ok()
|
||||
{
|
||||
set_status(wxString::Format(_L("OK — %zu triangles"),
|
||||
m_doc.display_mesh.its.indices.size()));
|
||||
const size_t n_tris = m_doc.display_mesh.its.indices.size();
|
||||
set_status(wxString::Format(_L_PLURAL("OK — %zu triangle", "OK — %zu triangles", unsigned(n_tris)), n_tris));
|
||||
if (m_viewport != nullptr) {
|
||||
// A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
|
||||
if (body_move_pending()) end_body_move(true);
|
||||
@@ -5035,11 +5040,15 @@ void DesignPanel::on_import_mesh()
|
||||
// A dense organic scan has few coplanar neighbours to merge away and stays heavy afterwards;
|
||||
// warn rather than silently freezing the CAD kernel on every subsequent recompute.
|
||||
if (mesh.its.indices.size() > MESH_IMPORT_TRIANGLE_WARN) {
|
||||
const int n_tris = int(mesh.its.indices.size());
|
||||
const wxString q = wxString::Format(
|
||||
_L("This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?"),
|
||||
int(mesh.its.indices.size()));
|
||||
_L_PLURAL("This mesh has %d triangle. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?",
|
||||
"This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
|
||||
"merging, so importing it may take a long time and leave a body that is slow to "
|
||||
"edit. Decimating the mesh first is usually better.\n\nImport anyway?", n_tris),
|
||||
n_tris);
|
||||
MessageDialog dlg(this, q, _L("Large mesh"), wxYES_NO | wxICON_WARNING);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
@@ -5080,7 +5089,7 @@ void DesignPanel::on_import_mesh()
|
||||
// source mesh the user needs to know about before they start cutting features into it.
|
||||
if (stats.is_solid) {
|
||||
set_status(StatusKind::Info, wxString::Format(
|
||||
_L("Imported solid — %d triangles → %d faces, volume %.2f mm³. Pick a face or edge, "
|
||||
_L("Imported solid — triangles: %d → faces: %d, volume %.2f mm³. Pick a face or edge, "
|
||||
"then Fillet / Cut / Shell to modify"),
|
||||
stats.kept_tris, stats.faces_final, stats.volume));
|
||||
} else {
|
||||
@@ -5171,7 +5180,8 @@ void DesignPanel::open_insert_card(const wxString& base_name)
|
||||
update_cards_frame(); m_form->Layout();
|
||||
m_form->FitInside();
|
||||
update_action_bar(); // surface the unified ✓/✗
|
||||
set_status(StatusKind::Info, base_name + _L(" — drag to place/size, then Confirm"));
|
||||
// TRN Design tab status; %s is the inserted item, such as "Text" or "SVG".
|
||||
set_status(StatusKind::Info, wxString::Format(_L("%s — drag to place/size, then Confirm"), base_name));
|
||||
}
|
||||
|
||||
// Confirm: keep the placed art and leave the placement gizmo. The feature is already in
|
||||
@@ -5255,7 +5265,7 @@ void DesignPanel::on_add_extrude()
|
||||
{
|
||||
BooleanMode mode = static_cast<BooleanMode>(m_mode->GetSelection()); // New/Add/Cut/Intersect
|
||||
m_feature_counter++;
|
||||
const std::string name = feature_name(_L("Extrude"));
|
||||
const std::string name = feature_name(_L_CONTEXT("Extrude", "Design"));
|
||||
int idx = -1;
|
||||
if (m_extrude_face_src >= 0) {
|
||||
// Onshape face-extrude: the picked solid face is the profile (no sketch wire).
|
||||
@@ -5383,7 +5393,7 @@ void DesignPanel::on_add_hole()
|
||||
|
||||
m_feature_counter++;
|
||||
const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane,
|
||||
feature_name(_L("Hole")));
|
||||
feature_name(_L_CONTEXT("Hole", "Design")));
|
||||
// On-face holes drill the body the face belongs to (even after the pick was cleared).
|
||||
if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size()))
|
||||
m_doc.features[hidx].target_body = m_hole_face_body;
|
||||
@@ -5770,7 +5780,7 @@ void DesignPanel::on_add_mirror()
|
||||
const BooleanMode mode = m_mirror_keep->GetValue() ? BooleanMode::New : BooleanMode::Add;
|
||||
m_feature_counter++;
|
||||
m_doc.add_mirror(plane_from_choice(m_mirror_plane->GetSelection()), target, mode,
|
||||
feature_name(_L("Mirror")));
|
||||
feature_name(_L_CONTEXT("Mirror", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -5817,7 +5827,7 @@ void DesignPanel::on_add_rib()
|
||||
}
|
||||
m_feature_counter++;
|
||||
m_doc.add_rib(sketch_ref, m_rib_entity->GetValue(), m_rib_thickness->GetValue(),
|
||||
m_rib_depth->GetValue(), target, feature_name(_L("Rib")));
|
||||
m_rib_depth->GetValue(), target, feature_name(_L_CONTEXT("Rib", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -5837,7 +5847,7 @@ void DesignPanel::on_add_project()
|
||||
m_feature_counter++;
|
||||
m_doc.add_project_edges(src_body, {}, face,
|
||||
plane_from_choice(m_proj_plane->GetSelection()),
|
||||
feature_name(_L("Project")));
|
||||
feature_name(_L_CONTEXT("Project", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -6024,7 +6034,7 @@ void DesignPanel::on_add_pattern()
|
||||
m_doc.add_pattern(circular, int(m_pattern_count->GetValue()),
|
||||
m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(),
|
||||
m_pattern_angle->GetValue(), target,
|
||||
feature_name(_L("Pattern")));
|
||||
feature_name(_L_CONTEXT("Pattern", "Design")));
|
||||
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
@@ -6126,7 +6136,7 @@ void DesignPanel::on_add_cut()
|
||||
m_feature_counter++;
|
||||
m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(),
|
||||
/*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true,
|
||||
m_cut_target->GetSelection(), feature_name(_L("Cut")));
|
||||
m_cut_target->GetSelection(), feature_name(_L_CONTEXT("Cut", "Design")));
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
else
|
||||
@@ -6429,7 +6439,8 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
using Mode = DesignSketchTool::Mode;
|
||||
auto pick_more = [](const wxString& lead, int n) {
|
||||
return n <= 0 ? lead
|
||||
: lead + wxString::Format(_L(" · %d picked"), n);
|
||||
// TRN Appended to a sketch tool prompt; %d is the number of picked entities.
|
||||
: lead + wxString::Format(_L_PLURAL(" · %d picked", " · %d picked", n), n);
|
||||
};
|
||||
switch (m) {
|
||||
case Mode::Select:
|
||||
@@ -6442,8 +6453,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
"Shift-click adds · Del removes");
|
||||
case Mode::Constrain:
|
||||
return picks > 0
|
||||
? wxString::Format(_L("%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)"), picks)
|
||||
? wxString::Format(_L_PLURAL("%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)",
|
||||
"%d picked · now choose a constraint (Horizontal, Parallel, "
|
||||
"Tangent, Equal…)", picks), picks)
|
||||
: _L("Constrain — click one or two entities, then choose a constraint");
|
||||
case Mode::Dimension:
|
||||
return step == 0 ? _L("Dimension — click an entity, or the first of two points")
|
||||
@@ -6544,8 +6557,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
|
||||
? _L("Mirror — first click the LINE to mirror about (a construction line works)")
|
||||
: picks == 0
|
||||
? _L("Mirror — axis set · now click the entities to mirror · Esc cancels")
|
||||
: wxString::Format(_L("Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies"), picks);
|
||||
: wxString::Format(_L_PLURAL("Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies",
|
||||
"Mirror — axis set · %d to mirror · click another to add or "
|
||||
"remove it · Enter applies", picks), picks);
|
||||
case Mode::Move:
|
||||
return step == 0 ? _L("Move — click the entities to move")
|
||||
: pick_more(_L("Move — drag the handle, or click the number to type the "
|
||||
@@ -6653,7 +6668,7 @@ wxString DesignPanel::offer_header(int kind) const
|
||||
case OfferSel::SkPoint: return _L("Sketch point");
|
||||
case OfferSel::Sk2Ent: {
|
||||
const int n = m_viewport ? m_viewport->sketch_selection_count() : 0;
|
||||
return wxString::Format(_L("%d sketch entities"), n);
|
||||
return wxString::Format(_L_PLURAL("%d sketch entity", "%d sketch entities", n), n);
|
||||
}
|
||||
default: return wxString();
|
||||
}
|
||||
@@ -6753,7 +6768,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
} else {
|
||||
offer_trace("row=%d %s DISABLED (%s)", row, kOfferRowNames[row],
|
||||
v->refusal ? v->refusal : "blocked");
|
||||
wxString s = tr(v->name);
|
||||
wxString s = I18N::translate(v->name, "Design");
|
||||
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
|
||||
append_offer_item(&menu, id, s, *v)->Enable(false);
|
||||
bound.push_back(nullptr);
|
||||
@@ -6828,7 +6843,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
}
|
||||
if (!ok) {
|
||||
offer_trace("row=%d %s ~ %s BLOCKED", row, kOfferRowNames[row], v->id);
|
||||
wxString s = tr(v->name);
|
||||
wxString s = I18N::translate(v->name, "Design");
|
||||
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
|
||||
append_offer_item(target, base + int(bound.size()), s, *v)->Enable(false);
|
||||
bound.push_back(nullptr);
|
||||
@@ -6935,8 +6950,8 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
|
||||
drop_ghost(); // the real bodies are about to become the ghost's pose
|
||||
m_doc.checkpoint(); // undo boundary: committing a mate from the offer
|
||||
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false,
|
||||
"Mate" + std::to_string(++m_feature_counter));
|
||||
++m_feature_counter;
|
||||
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, feature_name(_L("Mate")));
|
||||
if (idx < 0) {
|
||||
set_status(StatusKind::Error, _L("Mate rejected"));
|
||||
return;
|
||||
@@ -7165,7 +7180,7 @@ bool DesignPanel::on_add_plane()
|
||||
m_feature_counter++;
|
||||
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
|
||||
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
|
||||
feature_name(_L("Plane")));
|
||||
feature_name(_L_CONTEXT("Plane", "Design")));
|
||||
if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]);
|
||||
m_doc.recompute(); // datum-only docs yield no body; that is expected/benign
|
||||
set_status(StatusKind::Info, _L("Plane added — pick it as a sketch plane"));
|
||||
@@ -7206,7 +7221,7 @@ void DesignPanel::on_add_shell()
|
||||
|
||||
m_feature_counter++;
|
||||
m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body,
|
||||
feature_name(_L("Shell")));
|
||||
feature_name(_L_CONTEXT("Shell", "Design")));
|
||||
|
||||
if (!recompute_guarded(_L("Rebuilding model…")))
|
||||
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
|
||||
@@ -9500,7 +9515,8 @@ void DesignPanel::request_value(const wxString& label, double def, double mn, do
|
||||
m_form->FitInside();
|
||||
m_value_input->SetFocus();
|
||||
m_value_input->SetSelection(-1, -1); // select all so typing replaces the value
|
||||
set_status(StatusKind::Info, label + _L(" — type a value, press Enter (or Confirm)"));
|
||||
// TRN Design tab status; %s is the value being asked for, such as "Fillet radius".
|
||||
set_status(StatusKind::Info, wxString::Format(_L("%s — type a value, press Enter (or Confirm)"), label));
|
||||
}
|
||||
|
||||
void DesignPanel::confirm_value()
|
||||
@@ -10228,19 +10244,19 @@ void DesignPanel::on_commit()
|
||||
for (size_t b = 0; b < m_disp_body_meshes.size(); ++b) {
|
||||
if (b < m_body_visible.size() && !m_body_visible[b]) continue; // skip hidden
|
||||
if (m_disp_body_meshes[b].its.indices.empty()) continue;
|
||||
parts.emplace_back(&m_disp_body_meshes[b], wxString::FromUTF8("Design Body " + std::to_string(b + 1)));
|
||||
parts.emplace_back(&m_disp_body_meshes[b], wxString::Format(_L("Design Body %d"), int(b + 1)));
|
||||
}
|
||||
if (parts.empty()) { // every body hidden — nothing to ship
|
||||
set_status(_L("All bodies hidden — show one before committing"));
|
||||
return;
|
||||
}
|
||||
if (m_commit_mode == CommitMode::Assembly && parts.size() > 1)
|
||||
obj_list->load_mesh_object(parts, "Design Assembly");
|
||||
obj_list->load_mesh_object(parts, _L("Design Assembly"));
|
||||
else
|
||||
for (const auto& [mesh, name] : parts)
|
||||
obj_list->load_mesh_object(*mesh, name);
|
||||
} else {
|
||||
obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body");
|
||||
obj_list->load_mesh_object(m_disp_pick_mesh, _L("Design Body"));
|
||||
}
|
||||
|
||||
// Persist the editable parametric recipe alongside the committed meshes so the
|
||||
@@ -11478,7 +11494,8 @@ void DesignPanel::refresh_preview()
|
||||
|
||||
if (ok) {
|
||||
m_viewport->set_preview_mesh(mesh);
|
||||
set_status(StatusKind::Ok, wxString::Format(_L("Preview — %zu triangles"), mesh.its.indices.size()));
|
||||
const size_t n_tris = mesh.its.indices.size();
|
||||
set_status(StatusKind::Ok, wxString::Format(_L_PLURAL("Preview — %zu triangle", "Preview — %zu triangles", unsigned(n_tris)), n_tris));
|
||||
} else {
|
||||
m_viewport->clear_preview();
|
||||
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
|
||||
@@ -11789,13 +11806,13 @@ void DesignPanel::open_tool(Tool t)
|
||||
case Tool::Extrude: m_hdr_extrude->SetLabel(title(_L_CONTEXT("Extrude", "Design"))); break;
|
||||
case Tool::Dressup: m_hdr_dressup->SetLabel(title(
|
||||
m_dressup_type->GetSelection() == 0 ? _L("Fillet") : _L("Chamfer"))); break;
|
||||
case Tool::Hole: m_hdr_hole->SetLabel(title(_L("Hole"))); break;
|
||||
case Tool::Hole: m_hdr_hole->SetLabel(title(_L_CONTEXT("Hole", "Design"))); break;
|
||||
case Tool::Thread: m_hdr_thread->SetLabel(title(_L("Thread"))); break;
|
||||
case Tool::Shell: m_hdr_shell->SetLabel(title(_L_CONTEXT("Shell", "Design"))); break;
|
||||
case Tool::Revolve: m_hdr_revolve->SetLabel(title(_L("Revolve"))); break;
|
||||
case Tool::Sweep: m_hdr_sweep->SetLabel(title(_L("Sweep"))); break;
|
||||
case Tool::Pattern: m_hdr_pattern->SetLabel(title(_L_CONTEXT("Pattern", "Design"))); break;
|
||||
case Tool::Plane: m_hdr_plane->SetLabel(title(_L("Plane"))); break;
|
||||
case Tool::Plane: m_hdr_plane->SetLabel(title(_L_CONTEXT("Plane", "Design"))); break;
|
||||
case Tool::Loft: m_hdr_loft->SetLabel(title(_L("Loft"))); break;
|
||||
case Tool::Draft: m_hdr_draft->SetLabel(title(_L("Draft"))); break;
|
||||
case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break;
|
||||
@@ -11809,7 +11826,7 @@ void DesignPanel::open_tool(Tool t)
|
||||
case Tool::SurfaceOffset: m_hdr_surf_offset->SetLabel(title(_L("Surface Offset"))); break;
|
||||
case Tool::ThickenSurface: m_hdr_surf_thicken->SetLabel(title(_L("Thicken Surface"))); break;
|
||||
case Tool::Transform: m_hdr_transform->SetLabel(title(_L("Transform"))); break;
|
||||
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L("Mirror"))); break;
|
||||
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L_CONTEXT("Mirror", "Design"))); break;
|
||||
case Tool::Thicken: m_hdr_thicken->SetLabel(title(_L("Thicken"))); break;
|
||||
case Tool::Rib: m_hdr_rib->SetLabel(title(_L_CONTEXT("Rib", "Design"))); break;
|
||||
case Tool::Project: m_hdr_project->SetLabel(title(_L_CONTEXT("Project", "Design"))); break;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
@@ -902,8 +903,11 @@ void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged
|
||||
if (int(r.skipped.size()) != m_skipped_last) {
|
||||
m_skipped_last = int(r.skipped.size());
|
||||
if (m_skipped_last > 0)
|
||||
notify(format(_u8L("%1% constraint(s) cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and are ignored"), m_skipped_last), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and is ignored",
|
||||
"%1% constraints cannot act on this geometry (an ellipse or spline curve, "
|
||||
"or a zero-size circle) and are ignored", m_skipped_last)),
|
||||
m_skipped_last), false);
|
||||
}
|
||||
// Flag every entity referenced by a conflicting constraint so render() can
|
||||
// tint it red (Onshape/SolveSpace over-constrained feedback), and remember which
|
||||
@@ -8978,7 +8982,8 @@ void DesignSketchTool::confirm_op()
|
||||
|| self_len(d, a) || self_len(d, b);
|
||||
});
|
||||
if (released > 0)
|
||||
notify(format(_u8L("%1% constraint(s) the fillet/chamfer invalidated were removed"), released), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint the fillet/chamfer invalidated was removed",
|
||||
"%1% constraints the fillet/chamfer invalidated were removed", released)), released), false);
|
||||
auto coin = [&](R xr, int ln, R lr) {
|
||||
SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; };
|
||||
if (fillet) {
|
||||
@@ -9427,7 +9432,8 @@ void DesignSketchTool::confirm_transform()
|
||||
}
|
||||
});
|
||||
if (released > 0)
|
||||
notify(format(_u8L("%1% constraint(s) released by the transform"), released), false);
|
||||
notify(format(into_u8(_L_PLURAL("%1% constraint released by the transform",
|
||||
"%1% constraints released by the transform", released)), released), false);
|
||||
} else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
|
||||
// ADDITIVE: append copies of each subject, then bind each copy to its source. Lines
|
||||
// get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks
|
||||
|
||||
@@ -559,7 +559,7 @@ int HistoryWindow::get_extruder_id()
|
||||
void HistoryWindow::on_click_new_button(wxCommandEvent& event)
|
||||
{
|
||||
if (curr_obj && curr_obj->get_printer_series() == PrinterSeries::SERIES_P1P && m_calib_results_history.size() >= 16) {
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle."), 16), wxEmptyString, wxICON_WARNING | wxOK);
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle.", "This machine type can only hold %d historical results per nozzle.", 16), 16), wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -973,7 +973,7 @@ bool PressureAdvanceWizard::can_save_cali_result(const std::vector<PACalibResult
|
||||
}
|
||||
|
||||
if (curr_obj->get_printer_series() != PrinterSeries::SERIES_X1 && curr_obj->pa_calib_tab.size() >= MAX_PA_HISTORY_RESULTS_NUMS) {
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle. This result will not be saved."), MAX_PA_HISTORY_RESULTS_NUMS),
|
||||
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle. This result will not be saved.", "This machine type can only hold %d historical results per nozzle. This result will not be saved.", MAX_PA_HISTORY_RESULTS_NUMS), MAX_PA_HISTORY_RESULTS_NUMS),
|
||||
wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
return false;
|
||||
|
||||
@@ -352,7 +352,9 @@ void CalibrationCaliPage::update_subtask(MachineObject* obj)
|
||||
prepare_text = wxString::Format(_L("Cloud Slicing..."));
|
||||
}
|
||||
else {
|
||||
prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %s tasks ahead of you."), std::to_string(obj->queue_number));
|
||||
prepare_text = wxString::Format(_L_PLURAL("In Cloud Slicing Queue, there is %d task ahead of you.",
|
||||
"In Cloud Slicing Queue, there are %d tasks ahead of you.", obj->queue_number),
|
||||
obj->queue_number);
|
||||
show_percent = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ CaliPageButton::CaliPageButton(wxWindow* parent, CaliPageActionType type, wxStri
|
||||
this->SetLabel(_L("Finish"));
|
||||
break;
|
||||
default:
|
||||
this->SetLabel("Unknown");
|
||||
this->SetLabel(_L("Unknown"));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -644,9 +644,9 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
|
||||
|
||||
for (const auto& extruder : curr_obj->GetExtderSystem()->GetExtruders()) {
|
||||
if (extruder.GetNozzleType() == NozzleType::ntUndefine) {
|
||||
wxString name = _L("left");
|
||||
if (extruder.GetExtId() == 0) { name = _L("right"); }
|
||||
wxString msg = wxString::Format(_L("Printer %s nozzle information has not been set. Please configure it before proceeding with the calibration."), name);
|
||||
wxString msg = extruder.GetExtId() == 0 ?
|
||||
_L("Printer right nozzle information has not been set. Please configure it before proceeding with the calibration.") :
|
||||
_L("Printer left nozzle information has not been set. Please configure it before proceeding with the calibration.");
|
||||
MessageDialog msg_dlg(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK);
|
||||
msg_dlg.ShowModal();
|
||||
break;
|
||||
@@ -2815,7 +2815,7 @@ MaxVolumetricSpeedPresetPage::MaxVolumetricSpeedPresetPage(
|
||||
wxArrayString titles;
|
||||
titles.push_back(_L("From Volumetric Speed"));
|
||||
titles.push_back(_L("To Volumetric Speed"));
|
||||
titles.push_back(_L("Step"));
|
||||
titles.push_back(_L_CONTEXT("Step", "Increment"));
|
||||
m_custom_range_panel->set_titles(titles);
|
||||
|
||||
m_custom_range_panel->set_unit(_L("mm³/s"));
|
||||
|
||||
@@ -680,16 +680,15 @@ void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
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);
|
||||
const unsigned int max_count = static_cast<unsigned int>(m_max_filament_count);
|
||||
const wxString warning_text = m_selected_mode == DecomposeMode::MaterialList ?
|
||||
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
"The material list supports at most %1% colors. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
max_count), max_count) :
|
||||
// TRN %2% is the decomposition mode name, "CMYW" or "RYBW"
|
||||
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
|
||||
"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.",
|
||||
max_count), max_count, m_selected_mode == DecomposeMode::CMYW ? "CMYW" : "RYBW");
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -340,8 +341,9 @@ bool prepare_decompose_mixed_result(
|
||||
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);
|
||||
// TRN %1% is a color name such as "Cyan", %2% is a filament type such as "PLA Basic"
|
||||
missing_comp.display_name = format_wxstr(_L_CONTEXT("%1% %2%", "Color and filament type"),
|
||||
decompose_base_color_display(comp.base_color), wxString::FromUTF8(basic_type));
|
||||
missing.push_back(std::move(missing_comp));
|
||||
out_result.components.push_back(0);
|
||||
}
|
||||
@@ -407,8 +409,11 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition."),
|
||||
wxString message = wxString::Format(_L_PLURAL("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition.",
|
||||
"The current filament list does not contain %s. Project filaments required by "
|
||||
"the mixed filament will be created automatically after decomposition.",
|
||||
missing.size()),
|
||||
missing_text);
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "Widgets/WebViewHostDialog.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <variant>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
|
||||
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
|
||||
// and never offers more, so the picker, favorites and project colors all share one limit.
|
||||
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
|
||||
|
||||
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
|
||||
using ColorGradient = std::vector<ColorRGBA>;
|
||||
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
|
||||
|
||||
struct ColorPickerOptions {
|
||||
bool allow_gradient = false;
|
||||
bool allow_alpha = false;
|
||||
};
|
||||
|
||||
// Project storage preserves direction; FilamentColor's set is only for the
|
||||
// official palette and cannot represent ordered custom gradients.
|
||||
struct FilamentColorPickerValue {
|
||||
std::vector<std::string> colors;
|
||||
bool gradient = false;
|
||||
std::string primary;
|
||||
};
|
||||
|
||||
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
|
||||
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
|
||||
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
|
||||
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
|
||||
|
||||
class ColorPickerDialog : public WebViewHostDialog
|
||||
{
|
||||
public:
|
||||
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
|
||||
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
|
||||
~ColorPickerDialog() override;
|
||||
|
||||
const std::optional<ColorSelection>& selection() const { return m_selection; }
|
||||
|
||||
private:
|
||||
void add_user_scripts() override;
|
||||
void on_script_message(const nlohmann::json& payload) override;
|
||||
void handle_web_command(const nlohmann::json& payload);
|
||||
void send_initial_state();
|
||||
void finish(int return_code);
|
||||
void position_panel();
|
||||
void move_to_anchor();
|
||||
void resize_to_content(int height);
|
||||
void focus_webview();
|
||||
void repaint_webview();
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
void on_sys_color_changed() override;
|
||||
|
||||
ColorSelection m_initial;
|
||||
ColorPickerOptions m_options;
|
||||
std::optional<ColorSelection> m_selection;
|
||||
std::vector<ColorSelection> m_favorites;
|
||||
std::string m_page_id;
|
||||
bool m_preserve_multi_color = false;
|
||||
bool m_init_sent = false;
|
||||
bool m_page_ready = false;
|
||||
bool m_closing = false;
|
||||
bool m_favorites_writable = false;
|
||||
bool m_sidebar_open = false;
|
||||
std::string m_palette;
|
||||
bool m_initial_positioned = false;
|
||||
wxRect m_anchor_rect;
|
||||
wxRect m_work_area;
|
||||
int m_content_height = 360;
|
||||
int m_content_width = 330;
|
||||
bool m_positioning = false;
|
||||
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ConnectPrinter.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
|
||||
@@ -169,7 +170,7 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
|
||||
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
|
||||
{
|
||||
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
|
||||
if (code.empty())
|
||||
if (code.empty() && (!m_obj || m_obj->printer_agent_id != MOONRAKER_PRINTER_AGENT_ID))
|
||||
code = "88888888";
|
||||
for (char c : code) {
|
||||
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
|
||||
|
||||
@@ -35,7 +35,7 @@ CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
|
||||
m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL);
|
||||
m_list->SetMinSize(wxSize(50 * em, 18 * em));
|
||||
m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em);
|
||||
m_list->AppendColumn(_L_CONTEXT("Model", "Printer model"), wxLIST_FORMAT_LEFT, 8 * em);
|
||||
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
|
||||
|
||||
@@ -109,7 +109,8 @@ void CrealityDiscoveryDialog::run_discovery()
|
||||
"network and not blocked by Wi-Fi client isolation, then click Scan again."));
|
||||
} else {
|
||||
m_status->SetLabel(wxString::Format(
|
||||
_L("Found %zu Creality printer(s). Select one and click Use Selected."),
|
||||
_L_PLURAL("Found %zu Creality printer. Select it and click Use Selected.",
|
||||
"Found %zu Creality printers. Select one and click Use Selected.", m_rows.size()),
|
||||
m_rows.size()));
|
||||
m_list->Select(0);
|
||||
}
|
||||
|
||||
@@ -4690,7 +4690,7 @@ void EditFilamentPresetDialog::delete_preset()
|
||||
if (count > 0) {
|
||||
msg = _L("Presets inherited by other presets cannot be deleted");
|
||||
msg += "\n";
|
||||
msg += _L_PLURAL("The following presets inherits this preset.", "The following preset inherits this preset.", count);
|
||||
msg += _L_PLURAL("The following preset inherits this preset.", "The following presets inherit this preset.", count);
|
||||
wxString title = _L("Delete Preset");
|
||||
MessageDialog(this, msg + presets, title, wxOK | wxICON_ERROR).ShowModal();
|
||||
m_selected_printer.clear();
|
||||
|
||||
@@ -276,6 +276,18 @@ namespace Slic3r
|
||||
std::string connect_type = j["connect_type"].get<std::string>();
|
||||
std::string bind_state = j["bind_state"].get<std::string>();
|
||||
|
||||
// Passive discovery must not replace another agent's saved binding.
|
||||
if (get_current_printer_agent_id() == MOONRAKER_PRINTER_AGENT_ID) {
|
||||
if (AppConfig* config = get_app_config()) {
|
||||
const auto& machines = config->get_local_machines();
|
||||
auto it = machines.find(dev_id);
|
||||
if (it != machines.end() && it->second.printer_agent_id != MOONRAKER_PRINTER_AGENT_ID) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Ignoring Moonraker discovery for a device owned by another agent: " << dev_id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (connect_type == "farm") {
|
||||
connect_type ="lan";
|
||||
bind_state = "free";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "HMS.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/Utils/Http.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
@@ -67,6 +68,7 @@
|
||||
#include <wx/app.h>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
#include <wx/dir.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -528,6 +530,18 @@ bool MachineObject::HasRecentLanMessage()
|
||||
return diff.count() < 5000;
|
||||
}
|
||||
|
||||
bool MachineObject::has_access_right() const
|
||||
{
|
||||
return !m_access_revoked && (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID || !get_access_code().empty());
|
||||
}
|
||||
|
||||
void MachineObject::revoke_access()
|
||||
{
|
||||
// UI panels retain this object; revoke its binding without invalidating their pointers.
|
||||
access_code.clear();
|
||||
m_access_revoked = true;
|
||||
}
|
||||
|
||||
std::string MachineObject::get_access_code() const
|
||||
{
|
||||
return access_code;
|
||||
@@ -536,6 +550,7 @@ std::string MachineObject::get_access_code() const
|
||||
void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
{
|
||||
this->access_code = code;
|
||||
m_access_revoked = false;
|
||||
if (only_refresh) {
|
||||
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
|
||||
if (config) {
|
||||
@@ -549,25 +564,10 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
|
||||
// session boundary, since parse_user_print_info() always overwrites their code
|
||||
// fresh from the cloud API's current response, so there's no cross-agent leakage
|
||||
// risk to guard against there.
|
||||
if (!code.empty()) {
|
||||
if (has_access_right()) {
|
||||
DeviceManager::update_local_machine(*this, config);
|
||||
} else {
|
||||
// Only patch an existing record's code - don't persist a brand-new
|
||||
// never-bound entry just because set_access_code("") was called on it.
|
||||
const auto& machines = config->get_local_machines();
|
||||
auto it = machines.find(get_dev_id());
|
||||
if (it != machines.end()) {
|
||||
BBLocalMachine local_machine = it->second;
|
||||
local_machine.access_code = "";
|
||||
config->update_local_machine(local_machine);
|
||||
}
|
||||
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
|
||||
// old BBL-era code can't silently "re-bind" this device again via
|
||||
// get_access_code_with_legacy_fallback()'s legacy fallback.
|
||||
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
|
||||
config->erase("access_code", get_dev_id());
|
||||
config->erase("user_access_code", get_dev_id());
|
||||
}
|
||||
config->clear_local_machine_access_code(get_dev_id(), printer_agent_id);
|
||||
}
|
||||
} else {
|
||||
if (!code.empty())
|
||||
@@ -2685,7 +2685,7 @@ void MachineObject::update_print_progress(const json& value)
|
||||
|
||||
int MachineObject::connect()
|
||||
{
|
||||
if (get_dev_ip().empty()) return -1;
|
||||
if (m_access_revoked || get_dev_ip().empty()) return -1;
|
||||
std::string username = m_agent ? m_agent->default_lan_username() : std::string();
|
||||
std::string password = get_access_code();
|
||||
|
||||
@@ -2698,17 +2698,25 @@ int MachineObject::connect()
|
||||
std::string host = Http::get_host_from_url(input, &port);
|
||||
std::string ca_file;
|
||||
|
||||
if (GUI::wxGetApp().preset_bundle) {
|
||||
if (wxTheApp && GUI::wxGetApp().preset_bundle) {
|
||||
const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
if (port.empty())
|
||||
port = config.opt_string("printhost_port");
|
||||
ca_file = config.opt_string("printhost_cafile");
|
||||
|
||||
// Refresh credentials from the matching preset for connections that bypass GUI_App::select_machine().
|
||||
const std::string print_host = config.opt_string("print_host");
|
||||
if (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID && config.opt_string("printer_agent") == MOONRAKER_PRINTER_AGENT_ID &&
|
||||
!print_host.empty() && dev_id_from_address(print_host, config.opt_string("printhost_port")) == get_dev_id()) {
|
||||
password = config.opt_string("printhost_apikey");
|
||||
if (password != get_access_code())
|
||||
set_access_code(password);
|
||||
}
|
||||
}
|
||||
|
||||
if (host.empty())
|
||||
host = get_dev_ip();
|
||||
|
||||
|
||||
if (m_agent) {
|
||||
try {
|
||||
PrinterConnectionParams params{
|
||||
|
||||
@@ -119,6 +119,7 @@ private:
|
||||
std::string dev_name;
|
||||
std::string dev_ip;
|
||||
std::string access_code;
|
||||
bool m_access_revoked = false;
|
||||
|
||||
// type, time stamp, delay
|
||||
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
|
||||
@@ -229,7 +230,8 @@ public:
|
||||
std::string dev_connection_name; /* lan | eth */
|
||||
|
||||
/*access code*/
|
||||
bool has_access_right() const { return !get_access_code().empty(); }
|
||||
bool has_access_right() const;
|
||||
void revoke_access();
|
||||
std::string get_access_code() const;
|
||||
void set_access_code(std::string code, bool only_refresh = true);
|
||||
|
||||
|
||||
@@ -389,8 +389,8 @@ static void s_update_title(const std::shared_ptr<DevNozzleRack> rack, wgtDeviceN
|
||||
wxString title = _L("Induction Hotend Rack");
|
||||
if (rack && (rack->GetReadingCount() > 0))
|
||||
{
|
||||
wxString pending = ": " + _L("Reading") + wxString::Format(" %d/%d", rack->GetReadingIdx(), rack->GetReadingCount());
|
||||
title += pending;
|
||||
// TRN %d/%d is the hotend being read and the number of hotends to read.
|
||||
title = wxString::Format(_L("Induction Hotend Rack: Reading %d/%d"), rack->GetReadingIdx(), rack->GetReadingCount());
|
||||
}
|
||||
|
||||
title_label->SetLabel(title);
|
||||
|
||||
@@ -385,7 +385,8 @@ void EditGCodeDialog::selection_changed(wxDataViewEvent& evt)
|
||||
// Orca: move below checking for def in custom defined G-code placeholders
|
||||
// This allows custom placeholders to override the default ones for this dialog
|
||||
// Override custom def if selection is within the preset category
|
||||
if (!def || m_params_list->GetSelectedTopLevelCategory() == "Presets") {
|
||||
// The group label is translated, so compare against the translated label.
|
||||
if (!def || m_params_list->GetSelectedTopLevelCategory() == _L("Presets").ToStdString()) {
|
||||
const auto& full_config = wxGetApp().preset_bundle->full_config();
|
||||
if (const ConfigDef* config_def = full_config.def(); config_def && config_def->has(opt_key)) {
|
||||
def = config_def->get(opt_key);
|
||||
|
||||
+74
-165
@@ -2,6 +2,7 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Field.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "Plater.hpp"
|
||||
@@ -45,7 +46,11 @@
|
||||
#include <wx/arrstr.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/brush.h>
|
||||
#include <wx/pen.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <wx/image.h>
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dc.h>
|
||||
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
|
||||
|
||||
void ColourPicker::BUILD()
|
||||
{
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
|
||||
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
|
||||
// field the same height as other parameter buttons before drawing its swatch.
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
|
||||
|
||||
// Validate the color
|
||||
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk()) {
|
||||
clr = wxTransparentColour;
|
||||
}
|
||||
|
||||
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
|
||||
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
|
||||
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
|
||||
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
|
||||
window = button;
|
||||
button->SetBitmapMargins(0, 0);
|
||||
if (parent_is_custom_ctrl && m_opt.height < 0)
|
||||
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
|
||||
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
||||
convert_to_picker_widget(temp);
|
||||
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
|
||||
|
||||
// // recast as a wxWindow to fit the calling convention
|
||||
window = dynamic_cast<wxWindow*>(temp);
|
||||
|
||||
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
|
||||
#ifdef __WXMSW__
|
||||
temp,
|
||||
#endif
|
||||
this](wxCommandEvent e) {
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(temp, temp->GetColour());
|
||||
#endif
|
||||
on_change_field();
|
||||
}), temp->GetId());
|
||||
|
||||
// ORCA reset value to default on right click. previously no way to switch back on windows
|
||||
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
|
||||
#ifdef __WXMSW__
|
||||
temp->SetColour(wxTransparentColour);
|
||||
draw_bmp_btn(temp, wxTransparentColour);
|
||||
#else
|
||||
set_undef_value(temp);
|
||||
#endif
|
||||
on_change_field();
|
||||
e.Skip();
|
||||
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
|
||||
button->SetFont(wxGetApp().normal_font());
|
||||
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
set_value(boost::any(clr_str));
|
||||
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
|
||||
apply_user_color(wxTransparentColour);
|
||||
event.Skip();
|
||||
});
|
||||
|
||||
temp->SetToolTip(get_tooltip_text(clr_str));
|
||||
// Native layouts may assign the final field size after construction.
|
||||
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
|
||||
draw_bmp_btn();
|
||||
event.Skip();
|
||||
});
|
||||
button->SetToolTip(get_tooltip_text(clr_str));
|
||||
}
|
||||
|
||||
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
|
||||
void ColourPicker::draw_bmp_btn()
|
||||
{
|
||||
field->SetColour(wxTransparentColour);
|
||||
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
|
||||
wxImage image(btn->GetBitmap().GetSize());
|
||||
image.InitAlpha();
|
||||
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return;
|
||||
#ifdef __WXMSW__
|
||||
wxGCDC dc2(dc);
|
||||
#else
|
||||
wxDC &dc2(dc);
|
||||
#endif
|
||||
dc2.SetPen(wxPen("#F1754E", 1));
|
||||
|
||||
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
|
||||
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
|
||||
|
||||
dc.SelectObject(wxNullBitmap);
|
||||
btn->SetBitmapLabel(bmp);
|
||||
}
|
||||
|
||||
// ORCA match style with button on windows
|
||||
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
{
|
||||
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
|
||||
|
||||
if (!btn->GetBitmap().IsOk()) return;
|
||||
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
|
||||
btn->SetBackgroundColour(*wxWHITE);
|
||||
if (!window) return;
|
||||
auto* btn = static_cast<wxButton*>(window);
|
||||
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
|
||||
wxGetApp().UpdateDarkUI(btn);
|
||||
|
||||
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
|
||||
wxSize btn_sz = btn->GetSize();
|
||||
wxImage image(btn_sz);
|
||||
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
wxBitmap bmp(std::move(image));
|
||||
wxMemoryDC dc(bmp);
|
||||
if (!dc.IsOk()) return bmp;
|
||||
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
|
||||
wxGCDC dc2(dc);
|
||||
|
||||
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
|
||||
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
|
||||
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
|
||||
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
|
||||
|
||||
int padding = btn->FromDIP(5);
|
||||
dc2.SetPen(*wxTRANSPARENT_PEN);
|
||||
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
|
||||
return bmp;
|
||||
};
|
||||
|
||||
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
|
||||
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
|
||||
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
|
||||
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
|
||||
}
|
||||
|
||||
void ColourPicker::set_value(const boost::any& value, bool change_event)
|
||||
void ColourPicker::set_value(const std::string& value, bool change_event)
|
||||
{
|
||||
m_disable_change_event = !change_event;
|
||||
const wxString clr_str(boost::any_cast<wxString>(value));
|
||||
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
set_value(boost::any(wxString::FromUTF8(value)), change_event);
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
const wxColour parsed_clr(clr_str);
|
||||
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
|
||||
field->SetColour(clr);
|
||||
draw_bmp_btn(field, clr);
|
||||
#else
|
||||
wxColour clr(clr_str);
|
||||
if (clr_str.IsEmpty() || !clr.IsOk())
|
||||
set_undef_value(field);
|
||||
else
|
||||
field->SetColour(clr);
|
||||
#endif
|
||||
|
||||
m_disable_change_event = false;
|
||||
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
|
||||
{
|
||||
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
|
||||
const wxString clr_str = boost::any_cast<wxString>(value);
|
||||
const wxColour parsed(clr_str);
|
||||
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
boost::any& ColourPicker::get_value()
|
||||
{
|
||||
save_colors_to_config();
|
||||
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
|
||||
if (colour == wxTransparentColour)
|
||||
m_value = std::string("");
|
||||
else {
|
||||
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
|
||||
}
|
||||
return m_value;
|
||||
m_value = m_colour == wxTransparentColour ? std::string() :
|
||||
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
|
||||
{
|
||||
if (!color || !color->IsOk() || *color == m_colour) return;
|
||||
const std::string previous = boost::any_cast<std::string>(get_value());
|
||||
m_colour = *color;
|
||||
draw_bmp_btn();
|
||||
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
|
||||
}
|
||||
|
||||
void ColourPicker::msw_rescale()
|
||||
{
|
||||
Field::msw_rescale();
|
||||
|
||||
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
|
||||
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
|
||||
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
|
||||
if (m_opt.height >= 0)
|
||||
size.SetHeight(m_opt.height * m_em_unit);
|
||||
else if (parent_is_custom_ctrl && opt_height > 0)
|
||||
size.SetHeight(lround(opt_height * m_em_unit));
|
||||
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
|
||||
if (parent_is_custom_ctrl)
|
||||
field->SetSize(size);
|
||||
else
|
||||
field->SetMinSize(size);
|
||||
|
||||
#ifdef __WXMSW__
|
||||
draw_bmp_btn(field, field->GetColour());
|
||||
#else
|
||||
if (field->GetColour() == wxTransparentColour)
|
||||
set_undef_value(field);
|
||||
#endif
|
||||
|
||||
if (parent_is_custom_ctrl) window->SetSize(size);
|
||||
else window->SetMinSize(size);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::sys_color_changed()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (wxWindow* win = this->getWindow())
|
||||
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
|
||||
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
|
||||
draw_bmp_btn(picker, picker->GetColour());
|
||||
}
|
||||
#endif
|
||||
if (window) wxGetApp().UpdateDarkUI(window, true);
|
||||
draw_bmp_btn();
|
||||
}
|
||||
|
||||
void ColourPicker::on_button_click(wxCommandEvent &event) {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
|
||||
}
|
||||
}
|
||||
m_picker_widget->OnButtonClick(event);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
|
||||
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
|
||||
{
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
|
||||
if (m_picker_widget) {
|
||||
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
|
||||
m_clrData = m_picker_widget->GetColourData();
|
||||
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
|
||||
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
|
||||
ColorPickerDialog dialog(m_parent, color, {}, false, window);
|
||||
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
|
||||
const auto& result = std::get<ColorRGBA>(*dialog.selection());
|
||||
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
|
||||
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ColourPicker::save_colors_to_config() {
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
if (m_clrData) {
|
||||
std::vector<std::string> colors;
|
||||
if (colors.size() != CUSTOM_COLOR_COUNT) {
|
||||
colors.resize(CUSTOM_COLOR_COUNT);
|
||||
}
|
||||
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
|
||||
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
|
||||
}
|
||||
wxGetApp().app_config->save_custom_color_to_config(colors);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void PointCtrl::BUILD()
|
||||
|
||||
+10
-24
@@ -22,10 +22,10 @@
|
||||
#include <boost/any.hpp>
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/bmpcbox.h>
|
||||
#include <wx/clrpicker.h>
|
||||
#include <optional>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Config.hpp"
|
||||
@@ -610,41 +610,27 @@ private:
|
||||
};
|
||||
|
||||
class ColourPicker : public Field {
|
||||
using Field::Field;
|
||||
|
||||
void set_undef_value(wxColourPickerCtrl* field);
|
||||
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
|
||||
public:
|
||||
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
|
||||
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
|
||||
~ColourPicker() {}
|
||||
|
||||
wxWindow* window{ nullptr };
|
||||
void BUILD() override;
|
||||
|
||||
void set_value(const std::string& value, bool change_event = false) {
|
||||
m_disable_change_event = !change_event;
|
||||
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
|
||||
m_disable_change_event = false;
|
||||
}
|
||||
void set_value(const std::string& value, bool change_event = false);
|
||||
void set_value(const boost::any& value, bool change_event = false) override;
|
||||
boost::any& get_value() override;
|
||||
void msw_rescale() override;
|
||||
void sys_color_changed() override;
|
||||
|
||||
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
|
||||
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
|
||||
void enable() override { if (window) window->Enable(); }
|
||||
void disable() override{ if (window) window->Disable(); }
|
||||
wxWindow* getWindow() override { return window; }
|
||||
|
||||
private:
|
||||
void convert_to_picker_widget(wxColourPickerCtrl *widget);
|
||||
void on_button_click(wxCommandEvent &WXUNUSED(ev));
|
||||
void save_colors_to_config();
|
||||
private:
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
wxColourData* m_clrData{nullptr};
|
||||
wxColourPickerWidget* m_picker_widget{nullptr};
|
||||
#endif
|
||||
// An absent result is cancellation; transparent color is the undefined field.
|
||||
void apply_user_color(const std::optional<wxColour>& color);
|
||||
void draw_bmp_btn();
|
||||
void on_button_click(wxCommandEvent& event);
|
||||
wxColour m_colour = wxTransparentColour;
|
||||
};
|
||||
|
||||
class PointCtrl : public Field {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
|
||||
#include "EncodedFilament.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
@@ -278,19 +277,6 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
|
||||
return bdc.bitmap;
|
||||
}
|
||||
|
||||
// Sort colors by HSV values (primarily by hue, then saturation, then value)
|
||||
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
|
||||
if (colors.size() < 2) return;
|
||||
std::sort(colors.begin(), colors.end(),
|
||||
[](const wxColour& a, const wxColour& b) {
|
||||
ColourHSV ha = wxColourToHSV(a);
|
||||
ColourHSV hb = wxColourToHSV(b);
|
||||
if (ha.h != hb.h) return ha.h < hb.h;
|
||||
if (ha.s != hb.s) return ha.s < hb.s;
|
||||
return ha.v < hb.v;
|
||||
});
|
||||
}
|
||||
|
||||
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
|
||||
{
|
||||
// Check if color is transparent
|
||||
@@ -430,24 +416,16 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
|
||||
{
|
||||
if (colors.empty()) return wxNullBitmap;
|
||||
|
||||
// Make a copy to sort without modifying original
|
||||
std::vector<wxColour> sorted_colors = colors;
|
||||
|
||||
// Sort colors by HSV when there are 2 or more colors
|
||||
if (sorted_colors.size() >= 2) {
|
||||
sort_colors_by_hsv(sorted_colors);
|
||||
if (force_gradient && colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
|
||||
if (force_gradient && sorted_colors.size() >= 2) {
|
||||
return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
}
|
||||
|
||||
switch (sorted_colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(sorted_colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
|
||||
default: return create_gradient_filament_bitmap(sorted_colors, size);
|
||||
switch (colors.size()) {
|
||||
case 1: return create_single_filament_bitmap(colors[0], size);
|
||||
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
|
||||
case 3: return create_triple_filament_bitmap(colors, size);
|
||||
case 4: return create_quadruple_filament_bitmap(colors, size);
|
||||
default: return create_gradient_filament_bitmap(colors, size);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,9 +124,13 @@ void FilamentMapManualPanel::OnTimer(wxTimerEvent &)
|
||||
m_errors->Hide();
|
||||
m_suggestion_panel->Hide();
|
||||
} else {
|
||||
m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."),
|
||||
invalid_eid == 0 ? _L_CONTEXT("Left", "Nozzle position") : _L_CONTEXT("Right", "Nozzle position"),
|
||||
invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow")));
|
||||
// [extruder][nozzle volume type]
|
||||
static const char* const error_msgs[2][2] = {
|
||||
{L("Error: Left extruder has no available Standard nozzle, current group result is invalid."),
|
||||
L("Error: Left extruder has no available High Flow nozzle, current group result is invalid.")},
|
||||
{L("Error: Right extruder has no available Standard nozzle, current group result is invalid."),
|
||||
L("Error: Right extruder has no available High Flow nozzle, current group result is invalid.")}};
|
||||
m_errors->SetLabel(_(error_msgs[invalid_eid == 0 ? 0 : 1][invalid_nozzle == NozzleVolumeType::nvtStandard ? 0 : 1]));
|
||||
// Re-wrap: wrapping only applies to the label text present when Wrap is called,
|
||||
// and the label was empty at construction time.
|
||||
m_errors->Wrap(FromDIP(520));
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include "GUI.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
@@ -79,7 +83,8 @@ void FilamentPickerDialog::StartFlashing()
|
||||
m_flash_timer->Start(50);
|
||||
}
|
||||
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
|
||||
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color)
|
||||
: DPIDialog(parent ? parent : wxGetApp().mainframe,
|
||||
wxID_ANY,
|
||||
_L("Select Filament"),
|
||||
@@ -92,6 +97,7 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
|
||||
m_color_query = new FilamentColorCodeQuery();
|
||||
m_is_data_loaded = LoadFilamentData(fila_id);
|
||||
m_cur_filament_color = fila_color;
|
||||
m_ordered_color = std::move(ordered_color);
|
||||
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
|
||||
m_cur_color_name = new wxString(color_name);
|
||||
|
||||
@@ -441,6 +447,9 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
|
||||
|
||||
// Bind click
|
||||
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
|
||||
m_selection_changed = true;
|
||||
m_ordered_color.reset();
|
||||
m_custom_selection.reset();
|
||||
m_cur_filament_color = color_code->GetFilaColor();
|
||||
UpdatePreview(*color_code);
|
||||
UpdateButtonStates(btn);
|
||||
@@ -511,24 +520,27 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
|
||||
Layout();
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
|
||||
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
|
||||
{
|
||||
std::vector<wxColour> wx_colors = {custom_color};
|
||||
|
||||
// Update preview bitmap
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
|
||||
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
|
||||
|
||||
if (bmp.IsOk()) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
|
||||
m_color_demo->SetBitmap(bmp);
|
||||
m_color_demo->SetBackgroundColour(custom_color);
|
||||
m_color_demo->SetBackgroundColour(wx_colors.front());
|
||||
m_color_demo->Refresh();
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
|
||||
}
|
||||
|
||||
// Update preview labels for custom color
|
||||
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
|
||||
wxString label;
|
||||
for (const auto& color : wx_colors) {
|
||||
if (!label.empty()) label += " / ";
|
||||
label += color.GetAsString(wxC2S_HTML_SYNTAX);
|
||||
}
|
||||
m_label_preview_color->SetLabel(label);
|
||||
m_label_preview_idx->SetLabel("");
|
||||
Layout();
|
||||
}
|
||||
@@ -557,16 +569,6 @@ void FilamentPickerDialog::CreateMoreInfoButton()
|
||||
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
|
||||
}
|
||||
|
||||
wxColourData FilamentPickerDialog::GetSingleColorData()
|
||||
{
|
||||
wxColourData data;
|
||||
data.SetChooseFull(true);
|
||||
if (m_cur_filament_color.ColorCount() > 0) {
|
||||
data.SetColour(*m_cur_filament_color.m_colors.begin());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void FilamentPickerDialog::BindEvents()
|
||||
{
|
||||
// Bind mouse events for window dragging
|
||||
@@ -588,27 +590,37 @@ void FilamentPickerDialog::BindEvents()
|
||||
// Pause click detection while color picker is open
|
||||
StopClickDetection();
|
||||
|
||||
wxColourData original_data = GetSingleColorData();
|
||||
wxColourData result = show_sys_picker_dialog(this, original_data);
|
||||
|
||||
// Resume click detection after color picker closes
|
||||
StartClickDetection();
|
||||
|
||||
// Check if user actually selected a different color
|
||||
if (result.GetColour() != original_data.GetColour()) {
|
||||
wxColour selected_color = result.GetColour();
|
||||
|
||||
// Update m_current_filament_color with the selected color
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
m_cur_filament_color.m_colors.insert(selected_color);
|
||||
m_cur_filament_color.m_color_type = FilamentColor::ColorType::SINGLE_CLR;
|
||||
|
||||
// Update preview
|
||||
UpdateCustomColorPreview(selected_color);
|
||||
|
||||
// Clear currently selected button since custom color selected
|
||||
UpdateButtonStates(nullptr);
|
||||
FilamentColorPickerValue current;
|
||||
if (m_ordered_color) {
|
||||
current = *m_ordered_color;
|
||||
} else {
|
||||
for (const auto& color : m_cur_filament_color.m_colors)
|
||||
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
|
||||
}
|
||||
const auto initial = filament_color_picker_initial(current);
|
||||
std::optional<ColorSelection> selected;
|
||||
{
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
selected = dialog.selection();
|
||||
}
|
||||
StartClickDetection();
|
||||
if (!filament_color_picker_changed(initial, selected))
|
||||
return;
|
||||
|
||||
m_custom_selection = selected;
|
||||
m_ordered_color = filament_color_picker_result(*selected);
|
||||
m_selection_changed = true;
|
||||
std::vector<wxColour> colors;
|
||||
m_cur_filament_color.m_colors.clear();
|
||||
for (const auto& color : m_ordered_color->colors) {
|
||||
colors.emplace_back(wxString::FromUTF8(color));
|
||||
m_cur_filament_color.m_colors.insert(colors.back());
|
||||
}
|
||||
m_cur_filament_color.m_color_type = m_ordered_color->gradient ? FilamentColor::ColorType::GRADIENT_CLR : FilamentColor::ColorType::SINGLE_CLR;
|
||||
UpdateCustomColorPreview(colors, m_ordered_color->gradient);
|
||||
UpdateButtonStates(nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "FilamentBitmapUtils.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include <optional>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "EncodedFilament.hpp"
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -25,10 +26,13 @@ namespace Slic3r { namespace GUI {
|
||||
class FilamentPickerDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
|
||||
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
|
||||
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
|
||||
virtual ~FilamentPickerDialog();
|
||||
|
||||
// Public interface methods
|
||||
bool HasSelectionChanged() const { return m_selection_changed; }
|
||||
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
|
||||
bool IsDataLoaded() const { return m_is_data_loaded; }
|
||||
wxColour GetSelectedColour() const;
|
||||
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
|
||||
@@ -68,7 +72,7 @@ private:
|
||||
|
||||
// UI update methods
|
||||
void UpdatePreview(const FilamentColorCode& filament);
|
||||
void UpdateCustomColorPreview(const wxColour& custom_color);
|
||||
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
|
||||
void UpdateButtonStates(wxBitmapButton* selected_btn);
|
||||
|
||||
// Shaped window methods
|
||||
@@ -77,7 +81,6 @@ private:
|
||||
|
||||
// Data loading
|
||||
bool LoadFilamentData(const wxString& fila_id);
|
||||
wxColourData GetSingleColorData();
|
||||
|
||||
// Flash effect
|
||||
void StartFlashing();
|
||||
@@ -98,6 +101,9 @@ private:
|
||||
FilamentColorCodes* m_cur_color_codes{nullptr};
|
||||
wxBitmapButton* m_cur_selected_btn{nullptr};
|
||||
FilamentColor m_cur_filament_color;
|
||||
std::optional<FilamentColorPickerValue> m_ordered_color;
|
||||
std::optional<ColorSelection> m_custom_selection;
|
||||
bool m_selection_changed = false;
|
||||
|
||||
// Shaped window members
|
||||
wxBitmap m_shape_bmp;
|
||||
|
||||
@@ -1797,7 +1797,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
}
|
||||
else {*/
|
||||
//BBS: add only gcode mode
|
||||
if (m_only_gcode_in_preview) {
|
||||
// When the file's own settings were loaded into the presets, the bed already is the file's, with the
|
||||
// model and texture of a user or custom printer too, which the lookup below cannot resolve.
|
||||
if (m_only_gcode_in_preview && !wxGetApp().plater()->gcode_settings_loaded()) {
|
||||
Pointfs printable_area;
|
||||
//BBS: add bed exclude area
|
||||
Pointfs bed_exclude_area = Pointfs();
|
||||
@@ -3976,17 +3978,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
imgui.text(_u8L("Current grouping of slice result is not optimal."));
|
||||
wxString tip;
|
||||
if (delta_weight_to_best >= 0 && delta_change_to_best >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and %2% changes compared to optimal grouping."))
|
||||
% number_format(delta_weight_to_best)
|
||||
% delta_change_to_best).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and %2% change compared to optimal grouping.",
|
||||
"Increase %1%g filament and %2% changes compared to optimal grouping.", delta_change_to_best),
|
||||
number_format(delta_weight_to_best), delta_change_to_best);
|
||||
else if (delta_weight_to_best >= 0 && delta_change_to_best < 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to optimal grouping."))
|
||||
% number_format(delta_weight_to_best)
|
||||
% std::abs(delta_change_to_best)).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to optimal grouping.",
|
||||
"Increase %1%g filament and save %2% changes compared to optimal grouping.", std::abs(delta_change_to_best)),
|
||||
number_format(delta_weight_to_best), std::abs(delta_change_to_best));
|
||||
else if (delta_weight_to_best < 0 && delta_change_to_best >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to optimal grouping."))
|
||||
% number_format(std::abs(delta_weight_to_best))
|
||||
% delta_change_to_best).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to optimal grouping.",
|
||||
"Save %1%g filament and increase %2% changes compared to optimal grouping.", delta_change_to_best),
|
||||
number_format(std::abs(delta_weight_to_best)), delta_change_to_best);
|
||||
|
||||
imgui.text_wrapped(tip, parent_width);
|
||||
ImGui::PopStyleColor(1);
|
||||
@@ -3995,17 +3997,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
wxString tip;
|
||||
if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(delta_weight_to_single_ext)
|
||||
% delta_change_to_single_ext).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and %2% change compared to a printer with one nozzle.",
|
||||
"Save %1%g filament and %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
|
||||
number_format(delta_weight_to_single_ext), delta_change_to_single_ext);
|
||||
else if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext < 0)
|
||||
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(delta_weight_to_single_ext)
|
||||
% std::abs(delta_change_to_single_ext)).str());
|
||||
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to a printer with one nozzle.",
|
||||
"Save %1%g filament and increase %2% changes compared to a printer with one nozzle.", std::abs(delta_change_to_single_ext)),
|
||||
number_format(delta_weight_to_single_ext), std::abs(delta_change_to_single_ext));
|
||||
else if (delta_weight_to_single_ext < 0 && delta_change_to_single_ext >= 0)
|
||||
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to a printer with one nozzle."))
|
||||
% number_format(std::abs(delta_weight_to_single_ext))
|
||||
% delta_change_to_single_ext).str());
|
||||
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to a printer with one nozzle.",
|
||||
"Increase %1%g filament and save %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
|
||||
number_format(std::abs(delta_weight_to_single_ext)), delta_change_to_single_ext);
|
||||
|
||||
imgui.text_wrapped(tip, parent_width);
|
||||
}
|
||||
|
||||
+274
-71
@@ -1639,7 +1639,7 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
|
||||
m_shortcuts_assembly_view = {
|
||||
{_L("Left mouse button"), _L("Object selection")},
|
||||
{alt + _L("Left mouse button"), _L("Part selection")},
|
||||
{"1~16 " + _L("number keys"), _L("Number keys can quickly change the color of objects")},
|
||||
{_L("1~16 number keys"), _L("Number keys can quickly change the color of objects")},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1679,6 +1679,7 @@ GLCanvas3D::~GLCanvas3D()
|
||||
|
||||
m_sel_plate_toolbar.del_all_item();
|
||||
m_sel_plate_toolbar.del_stats_item();
|
||||
m_plate_switcher_items.clear();
|
||||
}
|
||||
|
||||
void GLCanvas3D::post_event(wxEvent &&event)
|
||||
@@ -2360,6 +2361,8 @@ void GLCanvas3D::zoom_to_gcode()
|
||||
void GLCanvas3D::zoom_to_plate(int plate_idx)
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE && _hides_inactive_plates())
|
||||
plate_idx = REQUIRES_ZOOM_TO_CUR_PLATE;
|
||||
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE) {
|
||||
box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
|
||||
box.min.z() = 0.0;
|
||||
@@ -2416,12 +2419,22 @@ float GLCanvas3D::get_collapse_toolbar_height() const
|
||||
}
|
||||
|
||||
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
|
||||
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
|
||||
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar,
|
||||
// get_canvas_toolbar_right and canvas_corner_height, so an overlay kept clear of the corner follows its layout.
|
||||
static constexpr float NAVIGATOR_SIZE = 128.f;
|
||||
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
|
||||
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
|
||||
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
|
||||
static constexpr float CANVAS_TOOLBAR_SPACING = 6.f;
|
||||
|
||||
// What the corner takes at the left edge of the canvas: the 3D navigator, or else the canvas toolbar.
|
||||
static float canvas_corner_height()
|
||||
{
|
||||
if (wxGetApp().show_3d_navigator())
|
||||
return NAVIGATOR_SIZE;
|
||||
const int buttons = wxGetApp().show_canvas_zoom_button() ? 3 : 2;
|
||||
return CANVAS_TOOLBAR_MARGIN + 2.f * CANVAS_TOOLBAR_PADDING + buttons * CANVAS_TOOLBAR_BUTTON + (buttons - 1) * CANVAS_TOOLBAR_SPACING;
|
||||
}
|
||||
|
||||
float GLCanvas3D::get_canvas_toolbar_right() const
|
||||
{
|
||||
@@ -2546,6 +2559,9 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
|
||||
wxGetApp().imgui()->new_frame();
|
||||
|
||||
if (_update_inactive_plates_visibility())
|
||||
scene_dirty = true;
|
||||
|
||||
if (m_picking_enabled && !m_benchmarking) {
|
||||
if (m_rectangle_selection.is_dragging())
|
||||
// picking pass using rectangle selection
|
||||
@@ -3053,10 +3069,9 @@ void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
|
||||
InstancesToZMap instances_min_z;
|
||||
|
||||
for (GLVolume* volume : m_volumes.volumes) {
|
||||
ModelObject* mo = m_model->objects[volume->object_idx()];
|
||||
ModelInstance* mi = mo->instances[volume->instance_idx()];
|
||||
ModelObject* mo = m_model->objects[volume->object_idx()];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4979,7 +4994,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
// Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
|
||||
// during the scene manipulation.
|
||||
|
||||
if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
|
||||
if (m_picking_enabled && (m_gizmos.is_allow_multi_select_parts_or_objects() || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
|
||||
if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
|
||||
int volume_idx = get_first_hover_volume_idx();
|
||||
bool already_selected = m_selection.contains_volume(volume_idx);
|
||||
@@ -4989,7 +5004,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
|
||||
if (already_selected && ctrl_down)
|
||||
m_selection.remove(volume_idx);
|
||||
else if (alt_down && m_gizmos.is_allow_multi_select_parts_or_objects() && m_selection.get_mode() == Selection::Volume &&
|
||||
m_selection.get_object_idx() == m_volumes.volumes[volume_idx]->object_idx() &&
|
||||
m_selection.get_instance_idx() == m_volumes.volumes[volume_idx]->instance_idx()) {
|
||||
// Alt+click on another part of the selected parts' instance adds it, and on a selected
|
||||
// part removes it, unless it is the only one.
|
||||
if (!already_selected)
|
||||
m_selection.add_volumes(Selection::Volume, {(unsigned int) volume_idx}, false);
|
||||
else if (curr_idxs.size() > 1)
|
||||
m_selection.remove_volumes(Selection::Volume, {(unsigned int) volume_idx});
|
||||
}
|
||||
else if (alt_down) {
|
||||
// Select just the clicked part, also when an object is selected.
|
||||
Selection::EMode mode = Selection::Volume;
|
||||
if (already_selected) {
|
||||
std::vector<unsigned int> volume_idxs;
|
||||
@@ -5521,7 +5547,7 @@ void GLCanvas3D::set_tooltip(const std::string& tooltip)
|
||||
m_tooltip.set_text(tooltip);
|
||||
}
|
||||
|
||||
void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_move)
|
||||
{
|
||||
if (m_model == nullptr)
|
||||
return;
|
||||
@@ -5553,10 +5579,18 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
|
||||
// Move instances/volumes
|
||||
ModelObject* model_object = m_model->objects[object_idx];
|
||||
if (model_object == nullptr)
|
||||
if (model_object == nullptr)
|
||||
continue;
|
||||
|
||||
if (selection_mode == Selection::Instance) {
|
||||
if (force_volume_move || selection_mode == Selection::Volume) {
|
||||
auto cur_mv = model_object->volumes[volume_idx];
|
||||
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
|
||||
cur_mv->set_transformation(v->get_volume_transformation());
|
||||
// BBS: backup
|
||||
Slic3r::save_object_mesh(*model_object);
|
||||
}
|
||||
}
|
||||
else if (selection_mode == Selection::Instance) {
|
||||
if (m_canvas_type == GLCanvas3D::ECanvasType::CanvasAssembleView) {
|
||||
if ((model_object->instances[instance_idx]->get_assemble_offset() - v->get_instance_offset()).norm() > 1e-2) {
|
||||
model_object->instances[instance_idx]->set_assemble_transformation(v->get_instance_transformation());
|
||||
@@ -5565,14 +5599,6 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation());
|
||||
}
|
||||
}
|
||||
else if (selection_mode == Selection::Volume) {
|
||||
auto cur_mv = model_object->volumes[volume_idx];
|
||||
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
|
||||
cur_mv->set_transformation(v->get_volume_transformation());
|
||||
// BBS: backup
|
||||
Slic3r::save_object_mesh(*model_object);
|
||||
}
|
||||
}
|
||||
|
||||
object_moved = true;
|
||||
model_object->invalidate_bounding_box();
|
||||
@@ -5589,13 +5615,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
// parts moved. Notifying a plate about an instance that stayed put invalidates its slice
|
||||
// result, and notifying instance 0 alone left a moved copy unregistered on its new plate.
|
||||
{
|
||||
const bool parts_moved = force_volume_move || selection_mode == Selection::Volume;
|
||||
std::set<std::pair<int, int>> notified;
|
||||
for (unsigned int i : m_selection.get_volume_idxs()) {
|
||||
const GLVolume* v = m_volumes.volumes[i];
|
||||
const int object_idx = v->object_idx();
|
||||
if (object_idx < 0 || object_idx >= static_cast<int>(m_model->objects.size()))
|
||||
continue;
|
||||
const std::pair<int, int> key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx());
|
||||
const std::pair<int, int> key(object_idx, parts_moved ? -1 : v->instance_idx());
|
||||
if (notified.insert(key).second)
|
||||
m_selection.notify_instance_update(key.first, key.second);
|
||||
}
|
||||
@@ -5604,9 +5631,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5735,9 +5761,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int> &i : done) {
|
||||
ModelObject *mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5838,9 +5863,8 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5959,9 +5983,8 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
ModelInstance* mi = mo->instances[i.second];
|
||||
|
||||
if (!mi->auto_drop) {
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -7662,7 +7685,6 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
|
||||
|
||||
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
{
|
||||
bool result = true;
|
||||
if (!m_sel_plate_toolbar.is_enabled()) {
|
||||
return false;
|
||||
}
|
||||
@@ -7687,19 +7709,13 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
|
||||
PartPlateList& plate_list = p_plater->get_partplate_list();
|
||||
for (int i = 0; i < plate_list.get_plate_count(); i++) {
|
||||
IMToolbarItem* item = new IMToolbarItem();
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
if (plate && plate->thumbnail_data.is_valid()) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
item->image_data = plate->thumbnail_data.pixels;
|
||||
item->image_width = plate->thumbnail_data.width;
|
||||
item->image_height = plate->thumbnail_data.height;
|
||||
result = item->generate_texture();
|
||||
}
|
||||
if (PartPlate* plate = plate_list.get_plate(i))
|
||||
item->set_thumbnail(plate->thumbnail_data);
|
||||
m_sel_plate_toolbar.m_items.push_back(item);
|
||||
}
|
||||
p_plater->clear_plate_toolbar_image_dirty();
|
||||
m_sel_plate_toolbar.is_display_scrollbar = false;
|
||||
return result;
|
||||
return true;
|
||||
}
|
||||
|
||||
//BBS: GUI refactor
|
||||
@@ -7967,8 +7983,8 @@ void GLCanvas3D::_picking_pass()
|
||||
if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) {
|
||||
const GLVolume* volume = m_volumes.volumes[hit.raycaster_id];
|
||||
if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
|
||||
// do not add the volume id if any gizmo is active and CTRL is pressed
|
||||
if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
|
||||
// do not add the volume id if CTRL is pressed while a gizmo that does not allow Ctrl+click selection is active
|
||||
if (m_gizmos.is_allow_multi_select_parts_or_objects() || !wxGetKeyState(WXK_CONTROL))
|
||||
m_hover_volume_idxs.emplace_back(hit.raycaster_id);
|
||||
m_gizmos.set_hover_id(-1);
|
||||
}
|
||||
@@ -8894,11 +8910,75 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
// With the other plates hidden, the axes go to the current plate as in Preview.
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled() && !_hides_inactive_plates());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_hides_inactive_plates() const
|
||||
{
|
||||
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_update_inactive_plates_visibility()
|
||||
{
|
||||
const bool hide = _hides_inactive_plates();
|
||||
if (!hide && !m_inactive_plates_hidden)
|
||||
return false;
|
||||
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
PartPlate* plate = hide ? plate_list.get_curr_plate() : nullptr;
|
||||
|
||||
struct Instance
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
bool selected{ false };
|
||||
};
|
||||
std::map<std::pair<int, int>, Instance> instances;
|
||||
if (plate != nullptr)
|
||||
for (const GLVolume* volume : m_volumes.volumes) {
|
||||
Instance& instance = instances[{ volume->object_idx(), volume->instance_idx() }];
|
||||
instance.box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
instance.selected |= volume->selected;
|
||||
}
|
||||
|
||||
// A gizmo that hides the other objects shows them all again when it closes.
|
||||
const bool keep_hidden = m_gizmos.is_hiding_instances();
|
||||
bool changed = false;
|
||||
bool any_hidden = false;
|
||||
std::vector<bool> shown_again(m_volumes.volumes.size(), false);
|
||||
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
|
||||
GLVolume* volume = m_volumes.volumes[i];
|
||||
const Instance* instance = plate != nullptr ? &instances[{ volume->object_idx(), volume->instance_idx() }] : nullptr;
|
||||
if (instance != nullptr && !instance->selected && instance->box.defined && !plate->get_bounding_box().overlap(instance->box)) {
|
||||
changed |= volume->is_active;
|
||||
volume->is_active = false;
|
||||
volume->on_hidden_plate = true;
|
||||
} else if (volume->on_hidden_plate && !keep_hidden) {
|
||||
volume->is_active = true;
|
||||
volume->on_hidden_plate = false;
|
||||
shown_again[i] = true;
|
||||
changed = true;
|
||||
}
|
||||
any_hidden |= volume->on_hidden_plate;
|
||||
}
|
||||
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters) {
|
||||
const int i = SceneRaycaster::decode_id(SceneRaycaster::EType::Volume, item->get_id());
|
||||
if (0 <= i && i < int(m_volumes.volumes.size()) && (m_volumes.volumes[i]->on_hidden_plate || shown_again[i]))
|
||||
item->set_active(shown_again[i]);
|
||||
}
|
||||
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Bed))
|
||||
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters)
|
||||
item->set_active(!hide || SceneRaycaster::decode_id(SceneRaycaster::EType::Bed, item->get_id()) / PartPlate::GRABBER_COUNT ==
|
||||
plate_list.get_curr_plate_index());
|
||||
|
||||
m_inactive_plates_hidden = hide || any_hidden;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
{
|
||||
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
@@ -8907,7 +8987,7 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
|
||||
PartPlate* curr_plate = plate_list.get_curr_plate();
|
||||
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
|
||||
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
|
||||
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled, _hides_inactive_plates());
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
|
||||
@@ -9925,6 +10005,7 @@ void GLCanvas3D::_render_overlays()
|
||||
|
||||
//BBS: GUI refactor: GLToolbar
|
||||
_render_imgui_select_plate_toolbar();
|
||||
_render_plate_switcher();
|
||||
_render_return_toolbar();
|
||||
// BBS
|
||||
//_render_view_toolbar();
|
||||
@@ -10193,6 +10274,35 @@ void GLCanvas3D::_render_main_toolbar()
|
||||
m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
|
||||
}
|
||||
|
||||
// The scrollbar of the plate toolbars of Preview and Prepare, in units of the toolbar scale.
|
||||
static constexpr float PLATE_TOOLBAR_SCROLLBAR = 14.f;
|
||||
|
||||
// Pushes 4 colors and 1 style var.
|
||||
static void push_plate_toolbar_scrollbar_style(bool is_dark, float scrollbar_size)
|
||||
{
|
||||
const ImVec4 grab(0.77f, 0.77f, 0.77f, is_dark ? .6f : 1.f);
|
||||
// A background with opacity over the window's would make a second color.
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, grab);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
}
|
||||
|
||||
// The border of a hovered plate in the plate toolbars, when it is not the current one.
|
||||
static ImVec4 plate_toolbar_hover_color(bool is_dark)
|
||||
{
|
||||
return ImVec4(0.67f, 0.67f, 0.67f, is_dark ? .6f : 1.f);
|
||||
}
|
||||
|
||||
// Not IsWindowHovered(), which is false while a button is held down and would drop the bar.
|
||||
static bool plate_toolbar_keeps_scrollbar(bool shown, float scrollbar_size)
|
||||
{
|
||||
const ImVec2 pos = ImGui::GetWindowPos();
|
||||
return ImGui::IsMouseHoveringRect(pos, pos + ImGui::GetWindowSize() + ImVec2(shown ? scrollbar_size : 0.f, 0.f), !shown) ||
|
||||
(ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow());
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
@@ -10323,16 +10433,15 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
|
||||
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
|
||||
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
|
||||
float scrollbar_size = 14.0f * f_scale;
|
||||
float scrollbar_size = PLATE_TOOLBAR_SCROLLBAR * f_scale;
|
||||
float window_height = std::min(window_height_calc, window_height_max);
|
||||
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
|
||||
|
||||
ImVec4 window_bg = m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f);
|
||||
ImVec4 button_active = ImGuiWrapper::COL_ORCA; // ORCA: Use orca color for selected sliced plate border
|
||||
ImVec4 button_hover = ImVec4(0.67f, 0.67f, 0.67, m_is_dark ? .6f : 1.0f);
|
||||
ImVec4 button_hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImVec4 orca_active = m_is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
|
||||
ImVec4 orca_hover = m_is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
|
||||
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f);
|
||||
ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
float button_radius = 1.0f * f_scale;
|
||||
@@ -10340,14 +10449,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
//use white text as the background switch to black
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, m_is_dark ? ImVec4(.9f, .9f, .9f, 1) : ImVec4(.3f, .3f, .3f, 1)); // ORCA Plate number text > Add support for dark mode
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hover);
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar_size);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
|
||||
@@ -10618,18 +10723,109 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
|
||||
ImGui::PopStyleColor(8);
|
||||
ImGui::PopStyleVar(7);
|
||||
|
||||
//ORCA ImGui::IsWindowHovered() returns false when left_down events on buttons that causes scrollbar disappears for a short time
|
||||
auto win_pos = ImGui::GetWindowPos();
|
||||
bool is_win_hovered = ImGui::IsMouseHoveringRect(win_pos, win_pos + ImVec2(window_width + (show_scroll ? scrollbar_size : 0), window_height), !show_scroll);
|
||||
|
||||
// Also show scrollbar visible and continue to capture mouse position
|
||||
const bool is_scrollbar_active_drag = GImGui != nullptr && ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow();
|
||||
|
||||
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered || is_scrollbar_active_drag;
|
||||
m_sel_plate_toolbar.is_display_scrollbar = plate_toolbar_keeps_scrollbar(show_scroll, scrollbar_size);
|
||||
|
||||
imgui.end();
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_plate_switcher()
|
||||
{
|
||||
Plater* plater = wxGetApp().plater();
|
||||
PartPlateList& plate_list = plater->get_partplate_list();
|
||||
const int plate_count = plate_list.get_plate_count();
|
||||
if (!_hides_inactive_plates() || plate_count < 2 || m_gizmos.get_current_type() != GLGizmosManager::Undefined)
|
||||
return;
|
||||
|
||||
// The thumbnails are the ones of the Preview plate toolbar.
|
||||
const unsigned version = plater->plate_toolbar_image_version();
|
||||
if (m_plate_switcher_version != version || m_plate_switcher_items.size() != size_t(plate_count)) {
|
||||
// Only the current plate is edited here; the others are redrawn when their thumbnails go invalid.
|
||||
for (int i = 0; i < plate_count && !plater->is_gcode_3mf(); ++i)
|
||||
plater->update_plate_thumbnails(i, i == plate_list.get_curr_plate_index());
|
||||
m_plate_switcher_items.clear();
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
m_plate_switcher_items.push_back(std::make_unique<IMToolbarItem>());
|
||||
m_plate_switcher_items.back()->set_thumbnail(plate_list.get_plate(i)->thumbnail_data);
|
||||
}
|
||||
m_plate_switcher_version = version;
|
||||
}
|
||||
|
||||
float sc = get_scale();
|
||||
#ifdef WIN32
|
||||
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
ImGuiWrapper& imgui = *wxGetApp().imgui();
|
||||
const ImVec2 btn_size = ImVec2(72.f, 72.f) * sc;
|
||||
const ImVec2 padding = ImVec2(8.f, 8.f) * sc;
|
||||
const float spacing = 8.f * sc;
|
||||
const float rounding = 4.f * sc;
|
||||
const float scrollbar = PLATE_TOOLBAR_SCROLLBAR * sc;
|
||||
const float top = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + CANVAS_TOOLBAR_MARGIN * sc;
|
||||
const float content_h = plate_count * (btn_size.y + spacing) - spacing + 2.f * padding.y;
|
||||
const float max_h = std::max(btn_size.y + 2.f * padding.y, float(get_canvas_size().get_height()) - top - (canvas_corner_height() + CANVAS_TOOLBAR_MARGIN) * sc);
|
||||
// As in the Preview plate toolbar, the scrollbar shows only while the mouse is over the column.
|
||||
const bool scroll = m_plate_switcher_scrollbar && content_h > max_h;
|
||||
|
||||
const ImVec4 text_color = m_is_dark ? ImVec4(.9f, .9f, .9f, 1.f) : ImVec4(.3f, .3f, .3f, 1.f);
|
||||
const ImVec4 empty_color = ImVec4(text_color.x, text_color.y, text_color.z, .45f);
|
||||
const ImVec4 hover = plate_toolbar_hover_color(m_is_dark);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
// Concentric with the thumbnails; tighter around the scrollbar, as in the Preview plate toolbar.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scroll ? 6.f * sc : rounding + padding.x);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f));
|
||||
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar);
|
||||
|
||||
imgui.set_next_window_pos(CANVAS_TOOLBAR_MARGIN * sc, top, ImGuiCond_Always, 0.f, 0.f);
|
||||
imgui.set_next_window_size(btn_size.x + 2.f * padding.x + (scroll ? scrollbar : 0.f), std::min(content_h, max_h), ImGuiCond_Always);
|
||||
imgui.begin(std::string("##PlateSwitcher"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoCollapse | (scroll ? 0 : ImGuiWindowFlags_NoScrollbar));
|
||||
|
||||
const int curr_plate = plate_list.get_curr_plate_index();
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
const ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
|
||||
const ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
|
||||
for (int i = 0; i < plate_count; ++i) {
|
||||
PartPlate* plate = plate_list.get_plate(i);
|
||||
const bool current = i == curr_plate;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushID(i);
|
||||
const bool clicked = ImGui::InvisibleButton("##plate", btn_size);
|
||||
ImGui::PopID();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_bg, rounding);
|
||||
if (ImTextureID texture = m_plate_switcher_items[i]->texture_id)
|
||||
draw_list->AddImageRounded(texture, pos, pos + btn_size, ImVec2(0.f, 1.f), ImVec2(1.f, 0.f), IM_COL32_WHITE, rounding);
|
||||
if (plate->empty())
|
||||
draw_list->AddRectFilled(pos, pos + btn_size, plate_dim, rounding);
|
||||
if (current || hovered) {
|
||||
const float border = 2.f * sc;
|
||||
draw_list->AddRect(pos - ImVec2(border, border) * .5f, pos + btn_size + ImVec2(border, border) * .5f,
|
||||
ImGui::GetColorU32(current ? (hovered ? ImGuiWrapper::COL_ORCA_HOVER : ImGuiWrapper::COL_ORCA) : hover),
|
||||
rounding + border * .5f, 0, border);
|
||||
}
|
||||
draw_list->AddText(pos + ImVec2(4.f, 2.f) * sc, ImGui::GetColorU32(plate->empty() ? empty_color : text_color), std::to_string(i + 1).c_str());
|
||||
|
||||
if (clicked && !current)
|
||||
plater->CallAfter([i]() {
|
||||
wxGetApp().plater()->select_plate(i);
|
||||
wxGetApp().plater()->deselect_all();
|
||||
});
|
||||
if (hovered) {
|
||||
const std::string name = plate->get_plate_name();
|
||||
imgui.tooltip(_u8L("Plate") + " " + std::to_string(i + 1) + (name.empty() ? "" : " (" + name + ")"), ImGui::GetFontSize() * 20.f);
|
||||
}
|
||||
}
|
||||
|
||||
m_plate_switcher_scrollbar = plate_toolbar_keeps_scrollbar(scroll, scrollbar);
|
||||
imgui.end();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(5);
|
||||
}
|
||||
|
||||
//BBS: GUI refactor: GLToolbar adjust
|
||||
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
|
||||
void GLCanvas3D::_render_assemble_view_toolbar() const
|
||||
@@ -10736,7 +10932,7 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
|
||||
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
|
||||
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
|
||||
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
|
||||
ImVec2 spacing = ImVec2(CANVAS_TOOLBAR_SPACING, CANVAS_TOOLBAR_SPACING) * sc;
|
||||
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
|
||||
Vec2i32 pos = {
|
||||
m_canvas_toolbar_pos[0] + margin.x,
|
||||
@@ -10939,6 +11135,17 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[]{wxGetApp().toggle_show_plate_gridlines();}
|
||||
);
|
||||
|
||||
create_menu_item( _utf8(L("Focus mode")),
|
||||
m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas, // work only on prepare
|
||||
wxGetApp().focus_mode(),
|
||||
[this]{
|
||||
wxGetApp().toggle_focus_mode();
|
||||
if (wxGetApp().focus_mode())
|
||||
zoom_to_plate(REQUIRES_ZOOM_TO_CUR_PLATE);
|
||||
m_dirty = true;
|
||||
}
|
||||
);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
create_menu_item( _utf8(L("Labels")),
|
||||
@@ -11195,7 +11402,7 @@ 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)) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f * f_scale);
|
||||
imgui.tooltip(_L("Shortcut Key ") + std::to_string(i + 1), ImGui::GetFontSize() * 20.0f);
|
||||
imgui.tooltip(wxString::Format(_L("Shortcut Key %d"), i + 1), ImGui::GetFontSize() * 20.0f);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
}
|
||||
@@ -12086,12 +12293,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
extruder_name += (boost::format(_u8L("Tool %d"))%extruder_id).str();
|
||||
}
|
||||
|
||||
if (error_iter->second.size() == 1) {
|
||||
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
else {
|
||||
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
|
||||
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
|
||||
error_iter->second.size()))) %filaments %extruder_name %extruder_name).str();
|
||||
}
|
||||
error = ErrorType::SLICING_LIMIT_ERROR;
|
||||
break;
|
||||
@@ -12139,11 +12343,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
}
|
||||
}
|
||||
std::string extruder_name = extruder_name_list[extruder_id-1];
|
||||
if (error_iter->second.size() == 1) {
|
||||
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
|
||||
} else {
|
||||
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
|
||||
}
|
||||
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
|
||||
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
|
||||
filament_ids.size()))) % filaments % extruder_name % extruder_name).str();
|
||||
}
|
||||
if (!text.empty()) {
|
||||
text += "\n";
|
||||
@@ -12170,7 +12372,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
warning += std::to_string(filament + 1);
|
||||
warning += " ";
|
||||
}
|
||||
text = (boost::format(_u8L("Filaments %s cannot be printed directly on the surface of this plate.")) % warning).str();
|
||||
text = (boost::format(into_u8(_L_PLURAL("Filament %s cannot be printed directly on the surface of this plate.",
|
||||
"Filaments %s cannot be printed directly on the surface of this plate.", conflict_filament.size()))) % warning).str();
|
||||
error = ErrorType::SLICING_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -660,6 +660,10 @@ private:
|
||||
mutable GLToolbar m_main_toolbar;
|
||||
mutable GLToolbar m_separator_toolbar;
|
||||
mutable IMToolbar m_sel_plate_toolbar;
|
||||
// Thumbnails of the plate switcher (Focus mode), refreshed when the plater's version changes.
|
||||
std::vector<std::unique_ptr<IMToolbarItem>> m_plate_switcher_items;
|
||||
unsigned m_plate_switcher_version{ unsigned(-1) };
|
||||
bool m_plate_switcher_scrollbar{ false };
|
||||
mutable GLToolbar m_assemble_view_toolbar;
|
||||
mutable IMReturnToolbar m_return_toolbar;
|
||||
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
|
||||
@@ -778,6 +782,8 @@ private:
|
||||
//BBS:add plate related logic
|
||||
mutable std::vector<int> m_hover_volume_idxs;
|
||||
std::vector<int> m_hover_plate_idxs;
|
||||
// Some volume or plate raycaster may still be hidden with the inactive plates.
|
||||
bool m_inactive_plates_hidden{ false };
|
||||
// IMEX ghost hover state (plate-owned transparent ghost volumes).
|
||||
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
|
||||
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
|
||||
@@ -1305,7 +1311,7 @@ public:
|
||||
void set_tooltip(const std::string& tooltip);
|
||||
|
||||
// the following methods add a snapshot to the undo/redo stack, unless the given string is empty
|
||||
void do_move(const std::string& snapshot_type);
|
||||
void do_move(const std::string& snapshot_type, bool force_volume_move = false);
|
||||
void do_rotate(const std::string& snapshot_type);
|
||||
void do_scale(const std::string& snapshot_type);
|
||||
void do_center();
|
||||
@@ -1527,6 +1533,9 @@ private:
|
||||
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add part plate related logic
|
||||
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
bool _hides_inactive_plates() const;
|
||||
// Returns whether what the scene shows changed.
|
||||
bool _update_inactive_plates_visibility();
|
||||
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
|
||||
BoundingBoxf3 _current_plate_box() const;
|
||||
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
|
||||
@@ -1565,6 +1574,8 @@ private:
|
||||
void _render_gizmos_overlay();
|
||||
void _render_main_toolbar();
|
||||
void _render_imgui_select_plate_toolbar();
|
||||
// Prepare: picks the plate to show while the inactive ones are hidden.
|
||||
void _render_plate_switcher();
|
||||
void _render_assemble_view_toolbar() const;
|
||||
void _render_return_toolbar() const;
|
||||
void _render_canvas_toolbar();
|
||||
|
||||
+305
-101
@@ -41,6 +41,7 @@
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <boost/locale/encoding_utf.hpp>
|
||||
#include <boost/log/detail/native_typeof.hpp>
|
||||
#include <cstdint>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
@@ -325,6 +326,13 @@ namespace GUI {
|
||||
|
||||
class MainFrame;
|
||||
|
||||
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
|
||||
// available (e.g. stealth mode or the network plugin still loading).
|
||||
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
|
||||
{
|
||||
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
|
||||
}
|
||||
|
||||
void start_ping_test()
|
||||
{
|
||||
return;
|
||||
@@ -1109,6 +1117,8 @@ void GUI_App::post_init()
|
||||
}
|
||||
|
||||
this->check_new_version_sf();
|
||||
if (!cw_showed)
|
||||
this->check_legacy_user_filament_presets(false);
|
||||
const auto cloud_provider = get_printer_cloud_provider();
|
||||
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
|
||||
// this->check_privacy_version(0);
|
||||
@@ -1147,14 +1157,8 @@ void GUI_App::post_init()
|
||||
if(!m_networking_need_update && m_agent) {
|
||||
m_agent->set_on_ssdp_msg_fn(
|
||||
[this](std::string json_str) {
|
||||
if (is_closing()) {
|
||||
return;
|
||||
}
|
||||
GUI::wxGetApp().CallAfter([this, json_str] {
|
||||
if (m_device_manager) {
|
||||
m_device_manager->on_machine_alive(json_str);
|
||||
}
|
||||
});
|
||||
if (!is_closing() && m_device_manager)
|
||||
m_device_manager->on_machine_alive(json_str);
|
||||
}
|
||||
);
|
||||
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
|
||||
@@ -1812,14 +1816,8 @@ void GUI_App::restart_networking()
|
||||
init_networking_callbacks();
|
||||
m_agent->set_on_ssdp_msg_fn(
|
||||
[this](std::string json_str) {
|
||||
if (is_closing()) {
|
||||
return;
|
||||
}
|
||||
GUI::wxGetApp().CallAfter([this, json_str] {
|
||||
if (m_device_manager) {
|
||||
m_device_manager->on_machine_alive(json_str);
|
||||
}
|
||||
});
|
||||
if (!is_closing() && m_device_manager)
|
||||
m_device_manager->on_machine_alive(json_str);
|
||||
}
|
||||
);
|
||||
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
|
||||
@@ -2272,7 +2270,9 @@ void GUI_App::init_networking_callbacks()
|
||||
if (return_code == 5) {
|
||||
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: the desktop lost its session on its own. Leave the webview's
|
||||
// own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
if (msg_dlg.ShowModal() == wxOK) {
|
||||
return;
|
||||
@@ -2616,7 +2616,10 @@ void GUI_App::init_download_path()
|
||||
}
|
||||
else {
|
||||
fs::path dp(down_path);
|
||||
if (!fs::exists(dp)) {
|
||||
// ORCA: non-throwing probe; ec is deliberately unread, a download_path that cannot be
|
||||
// probed falls back to the default like a missing one instead of failing startup.
|
||||
boost::system::error_code ec;
|
||||
if (!fs::exists(dp, ec)) {
|
||||
|
||||
std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data();
|
||||
app_config->set("download_path", user_down_path);
|
||||
@@ -3549,6 +3552,10 @@ bool GUI_App::on_init_inner()
|
||||
}
|
||||
|
||||
// BBS if load user preset failed
|
||||
// Let the libslic3r know the callback, which will translate messages on demand.
|
||||
// Installed before the presets load so that their load errors are translated too.
|
||||
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
|
||||
|
||||
//if (loaded_preset_result != 0) {
|
||||
try {
|
||||
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
|
||||
@@ -3566,9 +3573,6 @@ bool GUI_App::on_init_inner()
|
||||
register_win32_device_notification_event();
|
||||
#endif // WIN32
|
||||
|
||||
// Let the libslic3r know the callback, which will translate messages on demand.
|
||||
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
|
||||
|
||||
#if defined(__WXGTK__) && wxHAS_EGL
|
||||
// Configure GL backend before any wxGLCanvas is created.
|
||||
// On X11, prefer GLX for maximum driver compatibility.
|
||||
@@ -3922,81 +3926,74 @@ bool GUI_App::on_init_network(bool try_backup)
|
||||
if (config_version.empty()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download";
|
||||
m_networking_need_update = true;
|
||||
} else {
|
||||
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
|
||||
__retry:
|
||||
if (!load_agent_dll) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
|
||||
|
||||
if (!m_device_manager)
|
||||
m_device_manager = new Slic3r::DeviceManager();
|
||||
if (!m_user_manager)
|
||||
m_user_manager = new Slic3r::UserManager();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
|
||||
__retry:
|
||||
if (!load_agent_dll) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
|
||||
|
||||
std::string loaded_version = Slic3r::NetworkAgent::get_version();
|
||||
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
|
||||
// Self-heal only when a genuinely different series loaded than configured (e.g. the
|
||||
// configured build was unavailable and a fallback loaded). Within a series the
|
||||
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
|
||||
// compare series, not the raw string, and store the managed series form - a custom
|
||||
// config (02.08.01_custom) keeps its own name.
|
||||
std::string config_version = app_config->get_network_plugin_version();
|
||||
std::string loaded_series = network_plugin_series(loaded_version);
|
||||
if (network_plugin_series(config_version) != loaded_series) {
|
||||
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
|
||||
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
|
||||
app_config->set_network_plugin_version(synced);
|
||||
app_config->save();
|
||||
std::string loaded_version = Slic3r::NetworkAgent::get_version();
|
||||
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
|
||||
// Self-heal only when a genuinely different series loaded than configured (e.g. the
|
||||
// configured build was unavailable and a fallback loaded). Within a series the
|
||||
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
|
||||
// compare series, not the raw string, and store the managed series form - a custom
|
||||
// config (02.08.01_custom) keeps its own name.
|
||||
std::string config_version = app_config->get_network_plugin_version();
|
||||
std::string loaded_series = network_plugin_series(loaded_version);
|
||||
if (network_plugin_series(config_version) != loaded_series) {
|
||||
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
|
||||
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
|
||||
app_config->set_network_plugin_version(synced);
|
||||
app_config->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (check_networking_version()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
|
||||
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
|
||||
if (!bambu_source) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
|
||||
m_networking_compatible = false;
|
||||
if (check_networking_version()) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
|
||||
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
|
||||
if (!bambu_source) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
|
||||
m_networking_compatible = false;
|
||||
if (should_load_networking_plugin) {
|
||||
m_networking_need_update = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (try_backup) {
|
||||
int result = Slic3r::NetworkAgent::unload_network_module();
|
||||
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
|
||||
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
|
||||
try_backup = false;
|
||||
goto __retry;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
|
||||
if (should_load_networking_plugin) {
|
||||
m_networking_need_update = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (try_backup) {
|
||||
int result = Slic3r::NetworkAgent::unload_network_module();
|
||||
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
|
||||
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
|
||||
try_backup = false;
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
|
||||
// A failed install can leave the config naming a build that never made it to disk;
|
||||
// fall back to the installed latest instead of dropping the user into the re-download
|
||||
// flow. Only when the configured library is genuinely absent, though - a pinned series
|
||||
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
|
||||
std::string latest = get_latest_network_version();
|
||||
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
|
||||
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
|
||||
config_version = latest;
|
||||
app_config->set_network_plugin_version(latest);
|
||||
app_config->save();
|
||||
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
|
||||
goto __retry;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
|
||||
if (should_load_networking_plugin) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
|
||||
m_networking_need_update = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
|
||||
// A failed install can leave the config naming a build that never made it to disk;
|
||||
// fall back to the installed latest instead of dropping the user into the re-download
|
||||
// flow. Only when the configured library is genuinely absent, though - a pinned series
|
||||
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
|
||||
std::string latest = get_latest_network_version();
|
||||
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
|
||||
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
|
||||
config_version = latest;
|
||||
app_config->set_network_plugin_version(latest);
|
||||
app_config->save();
|
||||
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
|
||||
goto __retry;
|
||||
}
|
||||
if (should_load_networking_plugin) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
|
||||
m_networking_need_update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4024,6 +4021,11 @@ bool GUI_App::on_init_network(bool try_backup)
|
||||
else
|
||||
m_user_manager->set_agent(m_agent);
|
||||
|
||||
// A hot reload deletes the agent the task manager was built with. It lives until restart, so repoint it
|
||||
// even if multi-device has since been turned off.
|
||||
if (m_task_manager)
|
||||
m_task_manager->set_agent(m_agent);
|
||||
|
||||
if (this->is_enable_multi_machine()) {
|
||||
if (!m_task_manager) {
|
||||
m_task_manager = new Slic3r::TaskManager(m_agent);
|
||||
@@ -4228,8 +4230,10 @@ void GUI_App::switch_printer_agent()
|
||||
const std::string print_host = config.opt_string("print_host");
|
||||
if (!print_host.empty()) {
|
||||
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
|
||||
MachineObject* sel = m_device_manager->get_selected_machine();
|
||||
if (!sel || sel->get_dev_id() != dev_id)
|
||||
MachineObject* sel = m_device_manager->get_selected_machine();
|
||||
const bool access_code_changed = effective_agent_id == MOONRAKER_PRINTER_AGENT_ID && sel &&
|
||||
sel->get_access_code() != config.opt_string("printhost_apikey");
|
||||
if (!sel || sel->get_dev_id() != dev_id || access_code_changed)
|
||||
select_machine(effective_agent_id);
|
||||
}
|
||||
}
|
||||
@@ -4271,6 +4275,9 @@ void GUI_App::select_machine(const std::string& agent_id)
|
||||
return;
|
||||
}
|
||||
std::string port = host_cfg->opt_string("printhost_port");
|
||||
std::string access_code = host_cfg->opt_string("printhost_apikey");
|
||||
if (access_code.empty() && agent_id != MOONRAKER_PRINTER_AGENT_ID)
|
||||
access_code = "88888888";
|
||||
|
||||
// Generate dev_id from host and port
|
||||
std::string dev_id = MachineObject::dev_id_from_address(print_host, port);
|
||||
@@ -4297,11 +4304,6 @@ void GUI_App::select_machine(const std::string& agent_id)
|
||||
machine.dev_ip = dev_id;
|
||||
machine.dev_name = dev_id;
|
||||
machine.printer_type = preset.config.opt_string("printer_model");
|
||||
auto access_code = preset.config.opt_string("printhost_apikey");
|
||||
// Orca expect non empty access code
|
||||
if (access_code.empty()) {
|
||||
access_code = "88888888";
|
||||
}
|
||||
|
||||
existing = m_device_manager->insert_local_device(
|
||||
machine, "lan", "free", "", access_code);
|
||||
@@ -4311,6 +4313,9 @@ void GUI_App::select_machine(const std::string& agent_id)
|
||||
return;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id;
|
||||
} else if (agent_id == MOONRAKER_PRINTER_AGENT_ID && existing->printer_agent_id == agent_id &&
|
||||
(!existing->has_access_right() || existing->get_access_code() != access_code)) {
|
||||
existing->set_access_code(access_code);
|
||||
}
|
||||
existing->local_use_ssl = boost::istarts_with(print_host, "https://");
|
||||
|
||||
@@ -5243,7 +5248,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
|
||||
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
|
||||
{
|
||||
if (m_agent && m_agent->is_user_login(provider)) {
|
||||
m_agent->user_logout(true, provider);
|
||||
@@ -5256,6 +5261,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
|
||||
}
|
||||
|
||||
if (provider == ORCA_CLOUD_PROVIDER) {
|
||||
// Invalidate any handoff still in flight, so a token minted for the signed-out
|
||||
// account is never loaded into the pane.
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
|
||||
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
|
||||
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
|
||||
// loop through the code prompt on every desktop re-login.
|
||||
if (user_initiated) {
|
||||
if (auto orca = get_orca_cloud_agent(m_agent)) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
|
||||
}
|
||||
}
|
||||
|
||||
/* delete old user settings */
|
||||
bool transfer_preset_changes = false;
|
||||
wxString header = _L("Some presets are modified.") + "\n" +
|
||||
@@ -5377,7 +5397,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
|
||||
request_user_logout();
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_orca_login_info") == 0) {
|
||||
@@ -5392,7 +5412,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_bambu_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
|
||||
@@ -5401,7 +5421,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_orca_logout") == 0) {
|
||||
CallAfter([this] {
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_modeldepot") == 0) {
|
||||
@@ -5413,6 +5433,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
|
||||
else if (command_str.compare("homepage_openproject") == 0) {
|
||||
this->request_open_project({});
|
||||
}
|
||||
else if (command_str.compare("homepage_open_cloud") == 0) {
|
||||
// Deferred: set_cloud_mode() may create a second native webview, which must
|
||||
// not run inside this webview's script-message callback.
|
||||
CallAfter([this]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("homepage_close_cloud") == 0) {
|
||||
CallAfter([]() {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->set_cloud_mode(false);
|
||||
});
|
||||
}
|
||||
else if (command_str.compare("get_recent_projects") == 0) {
|
||||
if (mainframe) {
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
@@ -5685,7 +5719,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
|
||||
if (!provider.empty() && m_agent->is_user_login(provider)) {
|
||||
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
|
||||
this->request_user_logout(provider);
|
||||
// Involuntary: a 401 means the desktop session was rejected. Leave the
|
||||
// webview's own session alone (guide §8).
|
||||
this->request_user_logout(provider, /*user_initiated=*/false);
|
||||
|
||||
if (!m_show_http_error_msgdlg) {
|
||||
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
|
||||
@@ -5829,6 +5865,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
|
||||
}*/
|
||||
}
|
||||
|
||||
wxString GUI_App::cloud_webview_handoff()
|
||||
{
|
||||
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
|
||||
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
|
||||
// post request_web_session, which is what opens the desktop login dialog.
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return {};
|
||||
return from_u8(orca->build_web_session_start_url());
|
||||
}
|
||||
|
||||
void GUI_App::request_cloud_web_session(const std::string& state)
|
||||
{
|
||||
if (state.empty())
|
||||
return;
|
||||
|
||||
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
|
||||
m_cloud_handoff_state = state;
|
||||
const uint64_t gen = ++m_cloud_handoff_gen;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca || !orca->is_user_login()) {
|
||||
// Desktop signed out: open the desktop login dialog and stop here. On success
|
||||
// on_user_login_handle reloads the Start URL, which makes the page request again; on
|
||||
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
|
||||
// already stored, so a repeat request while the dialog is open is dropped rather than
|
||||
// stacking another dialog.
|
||||
if (!login_dlg || !login_dlg->IsShown())
|
||||
request_login(true, ORCA_CLOUD_PROVIDER);
|
||||
return;
|
||||
}
|
||||
|
||||
mint_cloud_web_session(state, gen);
|
||||
}
|
||||
|
||||
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
|
||||
{
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
|
||||
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
|
||||
boost::thread([this, orca, state, gen]() {
|
||||
std::string token_hash;
|
||||
std::string error_code;
|
||||
unsigned int http_code = 0;
|
||||
orca->request_web_session_handoff(token_hash, error_code, http_code);
|
||||
|
||||
if (is_closing())
|
||||
return;
|
||||
|
||||
if (token_hash.empty()) {
|
||||
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
|
||||
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
|
||||
// (it may echo account data), only the status/error code.
|
||||
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
|
||||
<< " error_code=" << error_code;
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
|
||||
on_cloud_web_session_minted(state, gen, token_hash);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
|
||||
{
|
||||
if (is_closing())
|
||||
return;
|
||||
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
|
||||
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
|
||||
return;
|
||||
if (token_hash.empty())
|
||||
return;
|
||||
|
||||
auto orca = get_orca_cloud_agent(m_agent);
|
||||
if (!orca)
|
||||
return;
|
||||
|
||||
// Load the Complete URL in the SAME webview that posted request_web_session, because the
|
||||
// state lives in that webview's session storage (guide §3/§7).
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
|
||||
}
|
||||
|
||||
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
{
|
||||
if (!m_agent) { return; }
|
||||
@@ -5884,6 +6007,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
|
||||
if (app_config->get("sync_user_preset") == "true") {
|
||||
start_sync_user_preset();
|
||||
}
|
||||
|
||||
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
|
||||
// without waiting for the next open. On a fresh login or account switch this invalidates any
|
||||
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
|
||||
// the page drops the old account and re-requests (guide §8).
|
||||
++m_cloud_handoff_gen;
|
||||
m_cloud_handoff_state.clear();
|
||||
if (WebViewPanel* home = WebViewPanel::if_built())
|
||||
if (home->cloud_mode_active())
|
||||
home->set_cloud_mode(true, cloud_webview_handoff());
|
||||
}
|
||||
|
||||
|
||||
@@ -6816,6 +6949,71 @@ void GUI_App::reload_settings()
|
||||
}
|
||||
}
|
||||
|
||||
void GUI_App::check_legacy_user_filament_presets(bool by_user)
|
||||
{
|
||||
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
|
||||
return;
|
||||
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
|
||||
if (legacy_presets.empty()) {
|
||||
if (by_user)
|
||||
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
|
||||
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
|
||||
return;
|
||||
}
|
||||
|
||||
wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare "
|
||||
"for future features. As a result, the presets below no longer show on some printers they were made "
|
||||
"for. Sorry for the trouble; this is a one-time fix.\n\n"
|
||||
"Add those printers back to your presets now? Your presets are backed up first. You can also do this "
|
||||
"later from Help > Migrate Filament Presets.") + "\n";
|
||||
for (const LegacyUserFilamentPreset &legacy : legacy_presets) {
|
||||
message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent);
|
||||
for (const std::string &printer : legacy.printers)
|
||||
message += "\n " + from_u8(printer);
|
||||
}
|
||||
RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION);
|
||||
if (!by_user)
|
||||
dlg.ShowCheckBox(_L("Don't remind me again"));
|
||||
const bool migrate = dlg.ShowModal() == wxID_YES;
|
||||
if (dlg.IsCheckBoxChecked())
|
||||
app_config->set_bool("legacy_user_filament_presets_dont_remind", true);
|
||||
if (!migrate)
|
||||
return;
|
||||
|
||||
const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets);
|
||||
const std::string &backup = migration.backup;
|
||||
const std::vector<std::string> &failed = migration.failed;
|
||||
if (backup.empty()) {
|
||||
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
|
||||
return;
|
||||
}
|
||||
if (!migration.locked) {
|
||||
// The lock is also refused for a while after a miss, and when the lock file itself cannot be opened.
|
||||
show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer "
|
||||
"instance may be saving presets; try again later from Help > Migrate Filament Presets."));
|
||||
return;
|
||||
}
|
||||
if (failed.size() == legacy_presets.size()) {
|
||||
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
|
||||
return;
|
||||
}
|
||||
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
|
||||
mainframe->update_side_preset_ui();
|
||||
for (auto tab : tabs_list) {
|
||||
tab->reload_config();
|
||||
tab->update_changed_ui();
|
||||
}
|
||||
|
||||
message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
|
||||
"A backup of your presets was saved to:\n%1%"), backup);
|
||||
if (!failed.empty()) {
|
||||
message += "\n\n" + _L("These presets could not be migrated:");
|
||||
for (const std::string &name : failed)
|
||||
message += "\n" + from_u8(name);
|
||||
}
|
||||
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
|
||||
}
|
||||
|
||||
//BBS reload when logout
|
||||
void GUI_App::remove_user_presets()
|
||||
{
|
||||
@@ -7952,8 +8150,11 @@ void GUI_App::on_stealth_mode_enter()
|
||||
{
|
||||
stop_sync_user_preset();
|
||||
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER);
|
||||
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
|
||||
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
|
||||
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
|
||||
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
|
||||
if (WebViewPanel* home = WebViewPanel::if_built()) {
|
||||
home->SendCloudProvidersInfo();
|
||||
}
|
||||
@@ -8582,22 +8783,23 @@ void GUI_App::refresh_plugins()
|
||||
// and report completion through the notification manager -- the speed dial needs no dialog.
|
||||
std::thread([]() {
|
||||
wxString error;
|
||||
bool unknown_error = false;
|
||||
try {
|
||||
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
|
||||
} catch (const std::exception& ex) {
|
||||
error = from_u8(ex.what());
|
||||
} catch (...) {
|
||||
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
|
||||
unknown_error = true; // translated in CallAfter: wx translation isn't safe off the UI thread
|
||||
}
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
wxTheApp->CallAfter([error]() {
|
||||
wxTheApp->CallAfter([error, unknown_error]() {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
Plater* plater = wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
return;
|
||||
if (error.IsEmpty())
|
||||
if (error.IsEmpty() && !unknown_error)
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
@@ -8606,7 +8808,7 @@ void GUI_App::refresh_plugins()
|
||||
plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::ErrorNotificationLevel,
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
|
||||
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), unknown_error ? _L("Unknown error") : error)));
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
@@ -10072,7 +10274,9 @@ void GUI_App::start_download(std::string url)
|
||||
}
|
||||
//lets always init so if the download dest folder was changed, new dest is used
|
||||
boost::filesystem::path dest_folder(app_config->get("download_path"));
|
||||
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) {
|
||||
// ORCA: non-throwing probe, as in init_download_path(); ec is deliberately unread.
|
||||
boost::system::error_code ec;
|
||||
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder, ec)) {
|
||||
std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard.");
|
||||
BOOST_LOG_TRIVIAL(error) << msg;
|
||||
show_error(nullptr, msg);
|
||||
|
||||
@@ -361,6 +361,11 @@ private:
|
||||
bool m_side_popup_status{false};
|
||||
bool m_show_http_error_msgdlg{false};
|
||||
std::chrono::steady_clock::time_point m_last_401_error_time;
|
||||
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
|
||||
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
|
||||
// newest request counts").
|
||||
std::string m_cloud_handoff_state;
|
||||
uint64_t m_cloud_handoff_gen { 0 };
|
||||
bool m_show_error_msgdlg{false};
|
||||
wxString m_info_dialog_content;
|
||||
HttpServer m_http_server;
|
||||
@@ -431,6 +436,8 @@ public:
|
||||
|
||||
bool show_plate_gridlines() const { return app_config->get_bool("show_plate_gridlines"); }
|
||||
void toggle_show_plate_gridlines() const { app_config->set_bool("show_plate_gridlines", !show_plate_gridlines()); }
|
||||
bool focus_mode() const { return app_config->get_bool("focus_mode"); }
|
||||
void toggle_focus_mode() const { app_config->set_bool("focus_mode", !focus_mode()); }
|
||||
|
||||
bool show_canvas_zoom_button() const { return app_config->get_bool("show_canvas_zoom_button"); }
|
||||
void toggle_canvas_zoom_button() const { app_config->set_bool("show_canvas_zoom_button", !show_canvas_zoom_button()); }
|
||||
@@ -540,7 +547,11 @@ public:
|
||||
|
||||
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
|
||||
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
|
||||
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
|
||||
// direction: a new caller that forgets the flag will not silently sign the webview out.
|
||||
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
|
||||
void post_logout_to_webview(const std::string& provider);
|
||||
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
|
||||
std::string handle_web_request(std::string cmd);
|
||||
@@ -556,6 +567,15 @@ public:
|
||||
void on_update_machine_list(wxCommandEvent& evt);
|
||||
void on_user_login(wxCommandEvent &evt);
|
||||
void on_user_login_handle(wxCommandEvent& evt);
|
||||
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
|
||||
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
|
||||
// called from the pane when the page asks for a session; it opens the desktop login dialog if
|
||||
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
|
||||
// in the same webview.
|
||||
wxString cloud_webview_handoff();
|
||||
void request_cloud_web_session(const std::string& state);
|
||||
void mint_cloud_web_session(const std::string& state, uint64_t gen);
|
||||
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
|
||||
void enable_user_preset_folder(bool enable);
|
||||
|
||||
// BBS
|
||||
@@ -567,6 +587,10 @@ public:
|
||||
void check_update(bool show_tips, int by_user);
|
||||
void check_new_version(bool show_tips = false, int by_user = 0);
|
||||
void check_new_version_sf(bool show_tips = false, int by_user = 0);
|
||||
// Offers to migrate user filament presets made against an older release that lost printers their parent
|
||||
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
|
||||
// reminded; by_user also reports when there is nothing to migrate.
|
||||
void check_legacy_user_filament_presets(bool by_user);
|
||||
bool process_network_msg(std::string dev_id, std::string msg);
|
||||
void request_new_version(int by_user);
|
||||
void enter_force_upgrade();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "Selection.hpp"
|
||||
//BBS: add partplate related logic
|
||||
#include "PartPlate.hpp"
|
||||
#include "Gizmos/GLGizmoAlignment.hpp"
|
||||
#include "Gizmos/GLGizmoEmboss.hpp"
|
||||
#include "Gizmos/GLGizmoSVG.hpp"
|
||||
|
||||
@@ -1672,6 +1673,8 @@ void MenuFactory::create_extra_object_menu()
|
||||
append_menu_item_merge_parts_to_single_part(&m_object_menu);
|
||||
// Object Center
|
||||
append_menu_item_center(&m_object_menu);
|
||||
// Object Align/Distribute
|
||||
append_menu_item_align_distribute(&m_object_menu);
|
||||
// Object Drop
|
||||
append_menu_item_drop(&m_object_menu);
|
||||
// Object Split
|
||||
@@ -1779,6 +1782,7 @@ void MenuFactory::create_text_part_menu()
|
||||
append_menu_item_fix_through_cgal(menu);
|
||||
append_menu_item_simplify(menu);
|
||||
append_menu_item_center(menu);
|
||||
append_menu_item_align_distribute(menu);
|
||||
append_menu_items_mirror(menu);
|
||||
menu->AppendSeparator();
|
||||
append_menu_item_per_object_process(menu);
|
||||
@@ -1811,6 +1815,7 @@ void MenuFactory::create_bbl_part_menu()
|
||||
append_menu_item_simplify(menu);
|
||||
append_menu_item_smooth_mesh(menu);
|
||||
append_menu_item_center(menu);
|
||||
append_menu_item_align_distribute(menu);
|
||||
append_menu_item_drop(menu);
|
||||
append_menu_items_mirror(menu);
|
||||
wxMenu* split_menu = new wxMenu();
|
||||
@@ -2136,6 +2141,7 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
index++;
|
||||
}
|
||||
append_menu_item_center(menu);
|
||||
append_menu_item_align_distribute(menu);
|
||||
append_menu_item_drop(menu);
|
||||
append_menu_item_fix_through_cgal(menu);
|
||||
//append_menu_item_simplify(menu);
|
||||
@@ -2144,13 +2150,13 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
|
||||
append_menu_item_set_printable(menu);
|
||||
menu->AppendSeparator();
|
||||
|
||||
|
||||
append_menu_item_set_auto_drop(menu);
|
||||
menu->AppendSeparator();
|
||||
|
||||
|
||||
append_menu_item_per_object_process(menu);
|
||||
menu->AppendSeparator();
|
||||
|
||||
|
||||
append_menu_items_convert_unit(menu);
|
||||
append_menu_item_replace_all_with_stl(menu);
|
||||
//BBS
|
||||
@@ -2161,6 +2167,7 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
}
|
||||
else {
|
||||
append_menu_item_center(menu);
|
||||
append_menu_item_align_distribute(menu);
|
||||
append_menu_item_drop(menu);
|
||||
append_menu_item_fix_through_cgal(menu);
|
||||
//append_menu_item_simplify(menu);
|
||||
@@ -2325,6 +2332,64 @@ void MenuFactory::append_menu_item_center(wxMenu* menu)
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_align_distribute(wxMenu *menu)
|
||||
{
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
auto can = [](AlignType type) {
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
|
||||
return type >= AlignType::DISTRIBUTE_X ? alignment_helper.can_distribute(type) : alignment_helper.can_align(type);
|
||||
};
|
||||
auto run = [](AlignType type) {
|
||||
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
|
||||
if (type >= AlignType::DISTRIBUTE_X)
|
||||
alignment_helper.distribute_objects(type);
|
||||
else
|
||||
alignment_helper.align_objects(type);
|
||||
};
|
||||
|
||||
struct AlignMenuItem
|
||||
{
|
||||
wxString label;
|
||||
AlignType type;
|
||||
std::string icon;
|
||||
};
|
||||
const AlignMenuItem items[] = {
|
||||
// icons shared with caanvas. BitmapCache converts colors dark to light. so dark variations used for proper colors
|
||||
{_L("Distribute left-right") + " (X)" , AlignType::DISTRIBUTE_X, "distribute_x_dark"},
|
||||
{_L("Distribute front-back") + " (Y)" , AlignType::DISTRIBUTE_Y, "distribute_y_dark"},
|
||||
{_L("Distribute top-bottom") + " (Z)" , AlignType::DISTRIBUTE_Z, "distribute_z_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align left") + " (-X)", AlignType::X_MIN, "align_x_min_dark"},
|
||||
{_L("Align left-right center") + " (X)" , AlignType::CENTER_X, "align_x_center_dark"},
|
||||
{_L("Align right") + " (+X)", AlignType::X_MAX, "align_x_max_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align front") + " (-Y)", AlignType::Y_MIN, "align_y_min_dark"},
|
||||
{_L("Align front-back center") + " (Y)" , AlignType::CENTER_Y, "align_y_center_dark"},
|
||||
{_L("Align back") + " (+Y)", AlignType::Y_MAX, "align_y_max_dark"},
|
||||
{wxString(), AlignType::NONE, ""},
|
||||
{_L("Align bottom") + " (-Z)", AlignType::Z_MIN, "align_z_min_dark"},
|
||||
{_L("Align top-bottom center") + " (Z)" , AlignType::CENTER_Z, "align_z_center_dark"},
|
||||
{_L("Align top") + " (+Z)", AlignType::Z_MAX, "align_z_max_dark"},
|
||||
};
|
||||
|
||||
wxMenu* align_distribute_menu = new wxMenu();
|
||||
for (const AlignMenuItem &item : items) {
|
||||
const AlignType type = item.type;
|
||||
if (type == AlignType::NONE)
|
||||
align_distribute_menu->AppendSeparator();
|
||||
else
|
||||
append_menu_item(
|
||||
align_distribute_menu, wxID_ANY, item.label, "", [run, type](wxCommandEvent &) { run(type); }, item.icon, nullptr,
|
||||
[can, type]() { return can(type); }, m_parent);
|
||||
}
|
||||
|
||||
// Anything that can be distributed can also be aligned, and aligning in X has no extra condition.
|
||||
append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "",
|
||||
[can]() { return can(AlignType::X_MIN); }, m_parent);
|
||||
}
|
||||
|
||||
void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
{
|
||||
append_menu_item(menu, wxID_ANY, _L("Drop") , "",
|
||||
@@ -2335,7 +2400,7 @@ void MenuFactory::append_menu_item_drop(wxMenu* menu)
|
||||
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
|
||||
return false;
|
||||
else {
|
||||
return (std::abs(plater()->get_view3D_canvas3D()->get_selection().get_bounding_box().min.z()) > -SINKING_Z_THRESHOLD);
|
||||
return plater()->get_view3D_canvas3D()->get_selection().can_drop();
|
||||
} //disable if model is on the bed / not in View3D
|
||||
}, m_parent);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ private:
|
||||
void append_menu_item_merge_to_single_object(wxMenu* menu);
|
||||
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
|
||||
void append_menu_items_mirror(wxMenu *menu);
|
||||
void append_menu_item_align_distribute(wxMenu *menu);
|
||||
void append_menu_item_precise_seam_submenu(wxMenu* menu);
|
||||
void append_menu_item_invalidate_cut_info(wxMenu *menu);
|
||||
void append_menu_item_edit_text(wxMenu *menu);
|
||||
|
||||
@@ -3234,8 +3234,8 @@ void ObjectList::merge(bool to_multipart_object)
|
||||
new_object->printable = false;
|
||||
new_object->instances[0]->printable = false;
|
||||
}
|
||||
if (object->instances[0]->auto_drop == false) {
|
||||
new_object->instances[0]->auto_drop = false;
|
||||
if (object->auto_drop == false) {
|
||||
new_object->auto_drop = false;
|
||||
}
|
||||
|
||||
// merge layers
|
||||
@@ -7098,18 +7098,10 @@ void ObjectList::toggle_auto_drop()
|
||||
|
||||
obj_idxs.emplace_back(static_cast<size_t>(obj_idx));
|
||||
|
||||
ItemType type = m_objects_model->GetItemType(item);
|
||||
// set auto_drop value for selected instance/instances in object
|
||||
if (type == itInstance) {
|
||||
int inst_idx = m_objects_model->GetInstanceIdByItem(item);
|
||||
obj->instances[inst_idx]->auto_drop = !current_auto_drop;
|
||||
} else {
|
||||
for (auto inst : obj->instances)
|
||||
inst->auto_drop = !current_auto_drop;
|
||||
|
||||
if (current_auto_drop == false)
|
||||
// auto_drop is shared by all instances of an object.
|
||||
obj->auto_drop = !current_auto_drop;
|
||||
if (current_auto_drop == false)
|
||||
obj->ensure_on_bed();
|
||||
}
|
||||
}
|
||||
|
||||
sort(obj_idxs.begin(), obj_idxs.end());
|
||||
|
||||
@@ -386,7 +386,7 @@ void GridCellFilamentsRenderer::Draw(wxGrid &grid, wxGridCellAttr &attr, wxDC &d
|
||||
dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y));
|
||||
}
|
||||
else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){
|
||||
dc.DrawText("Default", wxPoint(rect.x + offset_x, rect.y + offset_y));
|
||||
dc.DrawText(_L("Default"), wxPoint(rect.x + offset_x, rect.y + offset_y));
|
||||
}
|
||||
|
||||
text_rect.x += bitmap_width + grid_cell_border_width * 2;
|
||||
|
||||
@@ -0,0 +1,717 @@
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas)
|
||||
{
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent)
|
||||
{
|
||||
if (!can_align(type, align_parent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case AlignType::CENTER_X: return align_to_center(type, align_parent);
|
||||
case AlignType::CENTER_Y: return align_to_center(type, align_parent);
|
||||
case AlignType::CENTER_Z: return align_to_center(type, align_parent);
|
||||
case AlignType::Y_MAX: return align_to_y_max(align_parent);
|
||||
case AlignType::Y_MIN: return align_to_y_min(align_parent);
|
||||
case AlignType::X_MAX: return align_to_x_max(align_parent);
|
||||
case AlignType::X_MIN: return align_to_x_min(align_parent);
|
||||
case AlignType::Z_MAX: return align_to_z_max(align_parent);
|
||||
case AlignType::Z_MIN: return align_to_z_min(align_parent);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::distribute_objects(AlignType type)
|
||||
{
|
||||
if (!can_distribute(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case AlignType::DISTRIBUTE_X:
|
||||
return distribute_x();
|
||||
case AlignType::DISTRIBUTE_Y:
|
||||
return distribute_y();
|
||||
case AlignType::DISTRIBUTE_Z:
|
||||
return distribute_z();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_y_max(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.max.y(); },
|
||||
[](Vec3d& displacement, double target, double current_max) {
|
||||
displacement.y() = target - current_max;
|
||||
}, "Align Y Max", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_y_min(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.min.y(); },
|
||||
[](Vec3d& displacement, double target, double current_min) {
|
||||
displacement.y() = target - current_min;
|
||||
}, "Align Y Min", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_x_max(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.max.x(); },
|
||||
[](Vec3d& displacement, double target, double current_max) {
|
||||
displacement.x() = target - current_max;
|
||||
}, "Align X Max", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_x_min(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.min.x(); },
|
||||
[](Vec3d& displacement, double target, double current_min) {
|
||||
displacement.x() = target - current_min;
|
||||
}, "Align X Min", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
|
||||
{
|
||||
std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z");
|
||||
std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center");
|
||||
double selection_center = 0.f;
|
||||
if (align_parent) {
|
||||
BoundingBoxf3 parent_box = get_parent_box();
|
||||
selection_center = get_current_coord(operation_name, parent_box);
|
||||
|
||||
// Handle multiple volume selection
|
||||
if (is_parts_selection(true)) {
|
||||
if (!take_snapshot("Align to parent center " + type_str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
double current_coord = get_current_coord(operation_name, selection_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
// Move the selected volumes together so that their collective bounding box is centered in the parent
|
||||
for (const GLVolume *volume : get_selected_part_volumes()) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.empty()) return false;
|
||||
// Handle multiple objects or parts selection
|
||||
|
||||
if (!take_snapshot("Align to parent center " + type_str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
double current_coord = get_current_coord(operation_name, big_bb);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
// Multiple objects: use selection bounding box for alignment
|
||||
for (const auto &obj : objects) {
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
// Handle the parts of a single instance
|
||||
if (is_parts_selection(false)) {
|
||||
if (!take_snapshot("Align Parts Center " + type_str)){
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
selection_center = get_current_coord(operation_name, selection_bbox);
|
||||
|
||||
// Move each part to align to the center of their collective bounding box
|
||||
for (const GLVolume *volume : get_selected_part_volumes()) {
|
||||
auto temp_box = volume->transformed_convex_hull_bounding_box();
|
||||
double current = get_current_coord(operation_name, temp_box);
|
||||
double offset = selection_center - current;
|
||||
Vec3d displacement = generate_displacement(operation_name, offset);
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.empty()) return false;
|
||||
// Handle multiple instances
|
||||
selection_center = get_current_coord(operation_name, big_bb);
|
||||
|
||||
if (!take_snapshot("Align Objects Center" + type_str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
// Multiple objects: use selection bounding box for alignment
|
||||
for (const auto& obj : objects) {
|
||||
BoundingBoxf3 temp_bbox = obj.bbox;
|
||||
double current_coord = get_current_coord(operation_name, temp_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_z_max(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.max.z(); },
|
||||
[](Vec3d& displacement, double target, double current_max) {
|
||||
displacement.z() = target - current_max;
|
||||
}, "Align Z Max", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::align_to_z_min(bool align_parent)
|
||||
{
|
||||
return align_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.bbox.min.z(); },
|
||||
[](Vec3d& displacement, double target, double current_min) {
|
||||
displacement.z() = target - current_min;
|
||||
}, "Align Z Min", align_parent
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::distribute_y()
|
||||
{
|
||||
return distribute_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.center.y(); },
|
||||
1,
|
||||
"Distribute Y"
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::distribute_x()
|
||||
{
|
||||
return distribute_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.center.x(); },
|
||||
0,
|
||||
"Distribute X"
|
||||
);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::distribute_z()
|
||||
{
|
||||
return distribute_objects_generic(
|
||||
[](const ObjectInfo& obj) { return obj.center.z(); },
|
||||
2,
|
||||
"Distribute Z"
|
||||
);
|
||||
}
|
||||
|
||||
template<typename GetCoordFunc, typename SetCoordFunc>
|
||||
bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent)
|
||||
{
|
||||
if (align_parent) {
|
||||
// Find reference coordinate
|
||||
BoundingBoxf3 parent_box = get_parent_box();
|
||||
double reference_coord = get_current_coord(operation_name, parent_box);
|
||||
// Handle parts selection differently
|
||||
if (is_parts_selection(true)) {
|
||||
if (!take_snapshot("align to parent node " + operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
const std::vector<const GLVolume *> volumes = get_selected_part_volumes();
|
||||
if (volumes.empty())
|
||||
return false;
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
double current_coord = get_current_coord(operation_name, selection_bbox);
|
||||
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
|
||||
for (const GLVolume *volume : volumes) {
|
||||
if (displacement.norm() > 1e-6) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle objects selection (original logic)
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.empty()) return false;
|
||||
|
||||
if (!take_snapshot("align to parent node " + operation_name)) { return false; }
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
double current_coord = get_current_coord(operation_name, big_bb);
|
||||
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
|
||||
|
||||
for (const auto &obj : objects) {
|
||||
if (displacement.norm() > 1e-6) {
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
// Handle parts selection differently
|
||||
if (is_parts_selection(false)) {
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
|
||||
if (volumes.empty()) return false;
|
||||
|
||||
// Find reference coordinate
|
||||
double reference_coord = 0.0;
|
||||
bool first = true;
|
||||
|
||||
for (const GLVolume* volume : volumes) {
|
||||
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
|
||||
double coord = 0.0;
|
||||
|
||||
if (operation_name.find("X Max") != std::string::npos) {
|
||||
coord = bbox.max.x();
|
||||
} else if (operation_name.find("X Min") != std::string::npos) {
|
||||
coord = bbox.min.x();
|
||||
} else if (operation_name.find("Y Max") != std::string::npos) {
|
||||
coord = bbox.max.y();
|
||||
} else if (operation_name.find("Y Min") != std::string::npos) {
|
||||
coord = bbox.min.y();
|
||||
} else if (operation_name.find("Z Max") != std::string::npos) {
|
||||
coord = bbox.max.z();
|
||||
} else if (operation_name.find("Z Min") != std::string::npos) {
|
||||
coord = bbox.min.z();
|
||||
}
|
||||
|
||||
if (first) {
|
||||
reference_coord = coord;
|
||||
first = false;
|
||||
} else {
|
||||
if (operation_name.find("Max") != std::string::npos) {
|
||||
reference_coord = std::max(reference_coord, coord);
|
||||
} else if (operation_name.find("Min") != std::string::npos) {
|
||||
reference_coord = std::min(reference_coord, coord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const GLVolume* volume : volumes) {
|
||||
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
|
||||
double current_coord = 0.0;
|
||||
|
||||
if (operation_name.find("X Max") != std::string::npos) {
|
||||
current_coord = bbox.max.x();
|
||||
} else if (operation_name.find("X Min") != std::string::npos) {
|
||||
current_coord = bbox.min.x();
|
||||
} else if (operation_name.find("Y Max") != std::string::npos) {
|
||||
current_coord = bbox.max.y();
|
||||
} else if (operation_name.find("Y Min") != std::string::npos) {
|
||||
current_coord = bbox.min.y();
|
||||
} else if (operation_name.find("Z Max") != std::string::npos) {
|
||||
current_coord = bbox.max.z();
|
||||
} else if (operation_name.find("Z Min") != std::string::npos) {
|
||||
current_coord = bbox.min.z();
|
||||
}
|
||||
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
|
||||
if (operation_name.find("X") != std::string::npos) {
|
||||
displacement.x() = reference_coord - current_coord;
|
||||
} else if (operation_name.find("Y") != std::string::npos) {
|
||||
displacement.y() = reference_coord - current_coord;
|
||||
} else if (operation_name.find("Z") != std::string::npos) {
|
||||
displacement.z() = reference_coord - current_coord;
|
||||
}
|
||||
|
||||
if (displacement.norm() > 1e-6) {
|
||||
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle objects selection (original logic)
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.empty()) return false;
|
||||
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
double reference_coord = get_coord(objects[0]);
|
||||
for (const auto& obj : objects) {
|
||||
double coord = get_coord(obj);
|
||||
if (operation_name.find("Max") != std::string::npos) {
|
||||
reference_coord = std::max(reference_coord, coord);
|
||||
} else if (operation_name.find("Min") != std::string::npos) {
|
||||
reference_coord = std::min(reference_coord, coord);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
double current_coord = get_coord(obj);
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
set_coord(displacement, reference_coord, current_coord);
|
||||
|
||||
if (displacement.norm() > 1e-6) {
|
||||
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename GetCoordFunc>
|
||||
bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name)
|
||||
{
|
||||
if (is_parts_selection(false)) {
|
||||
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
|
||||
if (volumes.size() < 3) return false;
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
}
|
||||
get_selection().setup_cache();
|
||||
|
||||
struct VolEntry { const GLVolume* v; double coord; };
|
||||
std::vector<VolEntry> entries;
|
||||
entries.reserve(volumes.size());
|
||||
for (const GLVolume* v : volumes) {
|
||||
BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
|
||||
Vec3d c = bbox.center();
|
||||
double coord = (axis == 0 ? c.x() : axis == 1 ? c.y() : c.z());
|
||||
entries.push_back({v, coord});
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; });
|
||||
|
||||
BoundingBoxf3 selection_bbox = get_items_box();
|
||||
BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box();
|
||||
BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box();
|
||||
auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); };
|
||||
double left_width = axis_extent(left_bbox);
|
||||
double right_width = axis_extent(right_bbox);
|
||||
double bbox_min = (axis == 0 ? selection_bbox.min.x() : axis == 1 ? selection_bbox.min.y() : selection_bbox.min.z());
|
||||
double bbox_max = (axis == 0 ? selection_bbox.max.x() : axis == 1 ? selection_bbox.max.y() : selection_bbox.max.z());
|
||||
double min_center = bbox_min + left_width * 0.5;
|
||||
double max_center = bbox_max - right_width * 0.5;
|
||||
double total_distance = max_center - min_center;
|
||||
double interval = (entries.size() > 1) ? total_distance / (entries.size() - 1) : 0;
|
||||
for (size_t i = 1; i < entries.size() - 1; ++i) {
|
||||
double target_coord = min_center + i * interval;
|
||||
double current_coord = entries[i].coord;
|
||||
double offset = target_coord - current_coord;
|
||||
|
||||
if (std::abs(offset) > 1e-6) {
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
displacement[axis] = offset;
|
||||
const GLVolume *v = entries[i].v;
|
||||
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement, false);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation(true);
|
||||
return true;
|
||||
}
|
||||
BoundingBoxf3 big_bb;
|
||||
auto objects = get_selected_objects_info(big_bb);
|
||||
if (objects.size() < 3) return false;
|
||||
|
||||
if (!take_snapshot(operation_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get_selection().setup_cache();
|
||||
|
||||
std::sort(objects.begin(), objects.end(),
|
||||
[&get_coord](const ObjectInfo& a, const ObjectInfo& b) {
|
||||
return get_coord(a) < get_coord(b);
|
||||
});
|
||||
|
||||
double min_coord = get_coord(objects.front());
|
||||
double max_coord = get_coord(objects.back());
|
||||
double total_distance = max_coord - min_coord;
|
||||
double interval = (objects.size() > 1) ? total_distance / (objects.size() - 1) : 0;
|
||||
for (size_t i = 1; i < objects.size() - 1; ++i) {
|
||||
double target_coord = min_coord + i * interval;
|
||||
double current_coord = get_coord(objects[i]);
|
||||
double offset = target_coord - current_coord;
|
||||
|
||||
if (std::abs(offset) > 1e-6) {
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
displacement[axis] = offset;
|
||||
apply_transformation(objects[i].object_idx, objects[i].instance_idx, displacement);
|
||||
}
|
||||
}
|
||||
|
||||
finish_operation();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::can_align(AlignType type, bool align_parent) const
|
||||
{
|
||||
return get_items_count(type, align_parent) >= (align_parent ? 1 : 2);
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::can_distribute(AlignType type) const
|
||||
{
|
||||
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z) &&
|
||||
get_items_count(type, false) >= 3;
|
||||
}
|
||||
|
||||
double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox)
|
||||
{
|
||||
double current_coord = 0.0;
|
||||
if (operation_name.find("X Max") != std::string::npos) {
|
||||
current_coord = bbox.max.x();
|
||||
} else if (operation_name.find("X Min") != std::string::npos) {
|
||||
current_coord = bbox.min.x();
|
||||
} else if (operation_name.find("Y Max") != std::string::npos) {
|
||||
current_coord = bbox.max.y();
|
||||
} else if (operation_name.find("Y Min") != std::string::npos) {
|
||||
current_coord = bbox.min.y();
|
||||
} else if (operation_name.find("Z Max") != std::string::npos) {
|
||||
current_coord = bbox.max.z();
|
||||
} else if (operation_name.find("Z Min") != std::string::npos) {
|
||||
current_coord = bbox.min.z();
|
||||
} else if (operation_name.find("X Center") != std::string::npos) {
|
||||
current_coord = bbox.center().x();
|
||||
} else if (operation_name.find("Y Center") != std::string::npos) {
|
||||
current_coord = bbox.center().y();
|
||||
} else if (operation_name.find("Z Center") != std::string::npos) {
|
||||
current_coord = bbox.center().z();
|
||||
}
|
||||
return current_coord;
|
||||
}
|
||||
|
||||
Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double offset) {
|
||||
Vec3d displacement = Vec3d::Zero();
|
||||
if (operation_name.find("X") != std::string::npos) {
|
||||
displacement.x() = offset;
|
||||
} else if (operation_name.find("Y") != std::string::npos) {
|
||||
displacement.y() = offset;
|
||||
} else if (operation_name.find("Z") != std::string::npos) {
|
||||
displacement.z() = offset;
|
||||
}
|
||||
return displacement;
|
||||
}
|
||||
|
||||
std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
|
||||
{
|
||||
std::vector<ObjectInfo> objects;
|
||||
Selection& selection = get_selection();
|
||||
|
||||
if (selection.is_empty()) return objects;
|
||||
|
||||
const auto& content = selection.get_content();
|
||||
for (const auto& obj_it : content) {
|
||||
for (const auto& inst_idx : obj_it.second) {
|
||||
int obj_idx = obj_it.first;
|
||||
|
||||
if (obj_idx >= 0 && obj_idx < selection.get_model()->objects.size()) {
|
||||
ModelObject* object = selection.get_model()->objects[obj_idx];
|
||||
if (inst_idx >= 0 && inst_idx < object->instances.size()) {
|
||||
BoundingBoxf3 bbox = object->instance_bounding_box(inst_idx);
|
||||
big_bb.merge(bbox);
|
||||
objects.emplace_back(obj_idx, inst_idx, bbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::is_parts_selection(bool align_parent) const
|
||||
{
|
||||
const Selection &selection = get_selection();
|
||||
const auto &content = selection.get_content();
|
||||
if (content.size() != 1 || content.begin()->first >= 1000 || content.begin()->second.size() != 1)
|
||||
return false;
|
||||
return !align_parent || selection.get_mode() == Selection::Volume;
|
||||
}
|
||||
|
||||
std::vector<const GLVolume *> GLGizmoAlignment::get_selected_part_volumes() const
|
||||
{
|
||||
std::vector<const GLVolume *> volumes;
|
||||
if (!is_parts_selection(false))
|
||||
return volumes;
|
||||
|
||||
const Selection &selection = get_selection();
|
||||
const ModelObject *object = selection.get_model()->objects[selection.get_object_idx()];
|
||||
if (object->is_cut())
|
||||
return volumes;
|
||||
|
||||
for (unsigned int idx : selection.get_volume_idxs()) {
|
||||
const GLVolume *volume = selection.get_volume(idx);
|
||||
if (volume->volume_idx() < 0)
|
||||
continue;
|
||||
// Moving a whole instance's parts leaves its modifiers, negative volumes and support blockers in place,
|
||||
// as dragging a part does.
|
||||
if (selection.get_mode() == Selection::Volume || object->volumes[volume->volume_idx()]->is_model_part())
|
||||
volumes.push_back(volume);
|
||||
}
|
||||
return volumes;
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLGizmoAlignment::get_items_box() const
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
if (is_parts_selection(false)) {
|
||||
for (const GLVolume *volume : get_selected_part_volumes())
|
||||
box.merge(volume->transformed_convex_hull_bounding_box());
|
||||
} else
|
||||
get_selected_objects_info(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
BoundingBoxf3 GLGizmoAlignment::get_parent_box() const
|
||||
{
|
||||
const Selection &selection = get_selection();
|
||||
if (is_parts_selection(true))
|
||||
return selection.get_model()->objects[selection.get_object_idx()]->instance_convex_hull_bounding_box(selection.get_instance_idx());
|
||||
|
||||
// can_align() requires every selected instance on one plate, so the first one gives the plate.
|
||||
BoundingBoxf3 box;
|
||||
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
|
||||
if (obj_idx >= 1000 || inst_idxs.empty())
|
||||
continue;
|
||||
PartPlate *plate = plate_list.get_plate(plate_list.find_instance(obj_idx, *inst_idxs.begin()));
|
||||
if (plate == nullptr)
|
||||
break;
|
||||
BoundingBoxf3 instances_box;
|
||||
get_selected_objects_info(instances_box);
|
||||
box = plate->get_plate_box();
|
||||
box.max.z() = instances_box.max.z();
|
||||
// Keep the aligned objects off the plate edge.
|
||||
box.min += Vec3d(0.1, 0.1, 0.1);
|
||||
box.max -= Vec3d(0.1, 0.1, 0.1);
|
||||
break;
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
Selection& GLGizmoAlignment::get_selection() const
|
||||
{
|
||||
return m_canvas.get_selection();
|
||||
}
|
||||
|
||||
bool GLGizmoAlignment::take_snapshot(const std::string& name)
|
||||
{
|
||||
wxGetApp().plater()->take_snapshot(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement)
|
||||
{
|
||||
get_selection().translate(obj_idx, inst_idx, displacement);
|
||||
}
|
||||
|
||||
void GLGizmoAlignment::finish_operation(bool force_volume_move)
|
||||
{
|
||||
// A part move changes the ModelVolume shared by every copy of the object, so the other copies' volumes must show
|
||||
// it too, or do_move() would write their stale transformation back to the model.
|
||||
if (force_volume_move)
|
||||
get_selection().synchronize_unselected_volumes();
|
||||
// The operation already took its snapshot.
|
||||
m_canvas.do_move("", force_volume_move);
|
||||
}
|
||||
|
||||
size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const
|
||||
{
|
||||
auto cannot_move = [why_not](const std::string &reason) {
|
||||
if (why_not != nullptr)
|
||||
*why_not = reason;
|
||||
return size_t(0);
|
||||
};
|
||||
|
||||
if (is_parts_selection(align_parent)) {
|
||||
const size_t count = get_selected_part_volumes().size();
|
||||
return count > 0 ? count : cannot_move(_u8L("Parts of a cut object cannot be moved"));
|
||||
}
|
||||
|
||||
// Instances are moved through their instance offsets, which do_move() commits only from an Instance selection.
|
||||
const Selection &selection = get_selection();
|
||||
if (selection.get_mode() != Selection::Instance)
|
||||
return 0;
|
||||
|
||||
const bool is_z = type == AlignType::Z_MIN || type == AlignType::CENTER_Z || type == AlignType::Z_MAX ||
|
||||
type == AlignType::DISTRIBUTE_Z;
|
||||
// The plate gives no Z target, and aligning to its bottom would duplicate Drop.
|
||||
if (is_z && align_parent)
|
||||
return cannot_move(_u8L("Aligning to the plate is available in X and Y only"));
|
||||
|
||||
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
int plate_idx = -1;
|
||||
size_t count = 0;
|
||||
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
|
||||
if (obj_idx >= 1000)
|
||||
continue;
|
||||
// Auto-drop would put a raised object back on the bed and leave a lowered one sunk.
|
||||
if (is_z && selection.get_model()->objects[obj_idx]->auto_drop)
|
||||
return cannot_move(_u8L("Turn off Auto-Drop for the selected objects to move them in Z"));
|
||||
if (align_parent) {
|
||||
// The plate holding the selection is the target, so it must hold all of it.
|
||||
for (int inst_idx : inst_idxs) {
|
||||
const int idx = plate_list.find_instance(obj_idx, inst_idx);
|
||||
if (idx == -1 || (plate_idx != -1 && idx != plate_idx))
|
||||
return cannot_move(_u8L("All selected objects must be on the same plate"));
|
||||
plate_idx = idx;
|
||||
}
|
||||
}
|
||||
count += inst_idxs.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,102 @@
|
||||
#ifndef slic3r_GLGizmoAlignment_hpp_
|
||||
#define slic3r_GLGizmoAlignment_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class GLCanvas3D;
|
||||
|
||||
class GLGizmoAlignment
|
||||
{
|
||||
public:
|
||||
enum class AlignType {
|
||||
NONE = -1,
|
||||
CENTER_X,
|
||||
CENTER_Y,
|
||||
CENTER_Z,
|
||||
Y_MAX,
|
||||
Y_MIN,
|
||||
X_MAX,
|
||||
X_MIN,
|
||||
Z_MAX,
|
||||
Z_MIN,
|
||||
DISTRIBUTE_X,
|
||||
DISTRIBUTE_Y,
|
||||
DISTRIBUTE_Z
|
||||
};
|
||||
struct ObjectInfo {
|
||||
int object_idx;
|
||||
int instance_idx;
|
||||
BoundingBoxf3 bbox;
|
||||
Vec3d center;
|
||||
|
||||
ObjectInfo(int obj_idx, int inst_idx, const BoundingBoxf3& bb)
|
||||
: object_idx(obj_idx), instance_idx(inst_idx), bbox(bb), center(bb.center()) {}
|
||||
};
|
||||
|
||||
explicit GLGizmoAlignment(GLCanvas3D& canvas);
|
||||
~GLGizmoAlignment() = default;
|
||||
|
||||
bool align_objects(AlignType type,bool align_parent = false);
|
||||
bool distribute_objects(AlignType type);
|
||||
|
||||
bool align_to_center(AlignType type,bool align_parent = false);
|
||||
bool align_to_y_max(bool align_parent = false);
|
||||
bool align_to_y_min(bool align_parent = false);
|
||||
bool align_to_x_max(bool align_parent = false);
|
||||
bool align_to_x_min(bool align_parent = false);
|
||||
bool align_to_z_max(bool align_parent = false);
|
||||
bool align_to_z_min(bool align_parent = false);
|
||||
|
||||
bool distribute_x();
|
||||
bool distribute_y();
|
||||
bool distribute_z();
|
||||
|
||||
bool can_align(AlignType type, bool align_parent = false) const;
|
||||
bool can_distribute(AlignType type) const;
|
||||
|
||||
std::vector<ObjectInfo> get_selected_objects_info(BoundingBoxf3 &big_bb) const;
|
||||
double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox);
|
||||
Vec3d generate_displacement(std::string operation_name, double offset);
|
||||
|
||||
// A parts selection (one instance of one object) aligns the parts inside it; anything else aligns whole instances.
|
||||
// With align_parent, only a Volume-mode selection counts as parts: a whole instance aligns to its plate instead.
|
||||
bool is_parts_selection(bool align_parent) const;
|
||||
// The volumes a parts operation moves: the selected volumes, or only the model parts when a whole instance is
|
||||
// selected. Empty for a cut object, whose parts must keep their place.
|
||||
std::vector<const GLVolume *> get_selected_part_volumes() const;
|
||||
// World bounding box of the items an operation moves, the reference of every non-parent operation.
|
||||
BoundingBoxf3 get_items_box() const;
|
||||
// World bounding box aligned to with align_parent: the instance of a parts selection, or the plate holding the
|
||||
// selected instances, up to their top.
|
||||
BoundingBoxf3 get_parent_box() const;
|
||||
// Number of items the operation would move, or 0 when it cannot move them; why_not then gets the reason, if any.
|
||||
size_t get_items_count(AlignType type, bool align_parent, std::string *why_not = nullptr) const;
|
||||
|
||||
private:
|
||||
GLCanvas3D& m_canvas;
|
||||
|
||||
template<typename GetCoordFunc, typename SetCoordFunc>
|
||||
bool align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name, bool align_parent = false);
|
||||
|
||||
template<typename GetCoordFunc>
|
||||
bool distribute_objects_generic(GetCoordFunc get_coord, int axis,
|
||||
const std::string& operation_name);
|
||||
|
||||
Selection& get_selection() const;
|
||||
bool take_snapshot(const std::string& name);
|
||||
void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement);
|
||||
void finish_operation(bool force_volume_move = false);
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GLGizmoAlignment_hpp_
|
||||
@@ -818,7 +818,8 @@ void GLGizmoBrimEars::on_render_input_window(float x, float y, float bottom_limi
|
||||
}
|
||||
|
||||
if (has_invalid_ears) {
|
||||
wxString out = _L("Warning") + ": " + std::to_string(m_single_brim.size()) + " " + _L("invalid brim ears");
|
||||
const int n_invalid = int(m_single_brim.size());
|
||||
wxString out = _L("Warning") + ": " + wxString::Format(_L_PLURAL("%d invalid brim ear", "%d invalid brim ears", n_invalid), n_invalid);
|
||||
m_imgui->text_wrapped(out, parent_width);
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,6 @@ GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename,
|
||||
{"Flap Angle" , _u8L("Flap Angle")},
|
||||
{"Groove Angle" , _u8L("Groove Angle")},
|
||||
{"Cut position" , _u8L("Cut position")}, // ORCA
|
||||
{"Build Volume" , _u8L("Build Volume")}, // ORCA
|
||||
{"Multiple" , _u8L("Multiple")}, // ORCA
|
||||
{"Count" , _u8L("Count")}, // ORCA
|
||||
{"Gap" , _u8L("Gap")}, // ORCA
|
||||
@@ -2531,7 +2530,7 @@ void GLGizmoCut3D::render_build_size()
|
||||
Vec3d tbb_sz = m_transformed_bounding_box.size() * koef; // ORCA
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
m_imgui->text(_L("Build Volume"));
|
||||
m_imgui->text(_L("Size"));
|
||||
ImGui::SameLine(m_label_width);
|
||||
ImGui::Text("%.2f x %.2f x %.2f %s", tbb_sz.x(), tbb_sz.y(), tbb_sz.z(), unit_str.ToUTF8().data()); // ORCA use regular text color and simplify format
|
||||
}
|
||||
@@ -3555,10 +3554,14 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects)
|
||||
wxString names = from_u8(err_objects_names[0]);
|
||||
for (size_t i = 1; i < err_objects_names.size(); i++)
|
||||
names += ", " + from_u8(err_objects_names[i]);
|
||||
WarningDialog(wxGetApp().plater(), format_wxstr(_L("Objects(%1%) have duplicated connectors. "
|
||||
WarningDialog(wxGetApp().plater(), format_wxstr(_L_PLURAL("Object (%1%) has duplicated connectors. "
|
||||
"Some connectors may be missing in slicing result.\n"
|
||||
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
|
||||
"Thank you."), names)).ShowModal();
|
||||
"Thank you.",
|
||||
"Objects (%1%) have duplicated connectors. "
|
||||
"Some connectors may be missing in slicing result.\n"
|
||||
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
|
||||
"Thank you.", unsigned(err_objects_names.size())), names)).ShowModal();
|
||||
}
|
||||
|
||||
void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id)
|
||||
|
||||
@@ -898,9 +898,9 @@ void GLGizmoEmboss::on_render_input_window(float x, float y, float bottom_limit)
|
||||
|
||||
// Not known situation when could happend this is only for sure
|
||||
if (!m_is_unknown_font && !m_style_manager.is_active_font())
|
||||
create_notification_not_valid_font("No active font in style. Select correct one.");
|
||||
create_notification_not_valid_font(_u8L("No active font in style. Select correct one."));
|
||||
else if (!m_is_unknown_font && !m_style_manager.get_wx_font().IsOk())
|
||||
create_notification_not_valid_font("WxFont is not loaded properly.");
|
||||
create_notification_not_valid_font(_u8L("WxFont is not loaded properly."));
|
||||
|
||||
double screen_scale = wxDisplay(wxGetApp().plater()).GetScaleFactor();
|
||||
|
||||
|
||||
@@ -644,7 +644,8 @@ void GLGizmoFdmSupports::update_from_model_object(bool first_update)
|
||||
const TriangleMesh* mesh = &mv->mesh();
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false);
|
||||
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->supported_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
|
||||
//BBS: add timestamp logic
|
||||
|
||||
@@ -397,7 +397,8 @@ void GLGizmoFuzzySkin::update_from_model_object(bool first_update)
|
||||
const TriangleMesh* mesh = &mv->mesh();
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false);
|
||||
// Fuzzy skin paints a single state (FUZZY_SKIN): a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->fuzzy_skin_facets.get_data(), false, EnforcerBlockerType::FUZZY_SKIN);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1912,13 +1912,13 @@ void GLGizmoMeasure::show_selection_ui()
|
||||
float selection_cap_length = 0;
|
||||
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
|
||||
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
|
||||
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1" + _u8L(" (Moving)")).c_str()).x * 1.2;
|
||||
selection_cap_length = std::max(ImGui::CalcTextSize(_u8L("Face 1 (Fixed)").c_str()).x, ImGui::CalcTextSize(_u8L("Face 2 (Moving)").c_str()).x) * 1.2;
|
||||
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
|
||||
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1" + _u8L(" (Moving)")).c_str()).x * 1.2;
|
||||
selection_cap_length = std::max(ImGui::CalcTextSize(_u8L("Point 1 (Fixed)").c_str()).x, ImGui::CalcTextSize(_u8L("Point 2 (Moving)").c_str()).x) * 1.2;
|
||||
}
|
||||
}
|
||||
else {
|
||||
selection_cap_length = ImGui::CalcTextSize((_u8L("Selection") + " 1").c_str()).x * 1.2;
|
||||
selection_cap_length = ImGui::CalcTextSize(_u8L("Selection 1").c_str()).x * 1.2;
|
||||
}
|
||||
auto feature_first_text = format_item_text(m_selected_features.first);
|
||||
const float feature_first_text_length = ImGui::CalcTextSize((_u8L(feature_first_text)).c_str()).x;
|
||||
@@ -1934,13 +1934,13 @@ void GLGizmoMeasure::show_selection_ui()
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(SELECTED_1ST_COLOR));
|
||||
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
|
||||
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
|
||||
m_imgui->text(_u8L("Face") + " 1" + _u8L(" (Fixed)"));
|
||||
m_imgui->text(_u8L("Face 1 (Fixed)"));
|
||||
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
|
||||
m_imgui->text(_u8L("Point") + " 1" + _u8L(" (Fixed)"));
|
||||
m_imgui->text(_u8L("Point 1 (Fixed)"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
m_imgui->text(_u8L("Selection") + " 1");
|
||||
m_imgui->text(_u8L("Selection 1"));
|
||||
}
|
||||
ImGui::SameLine(selection_cap_length + space_size);
|
||||
ImGui::PushItemWidth(feature_first_text_length);
|
||||
@@ -1960,12 +1960,12 @@ void GLGizmoMeasure::show_selection_ui()
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(SELECTED_2ND_COLOR));
|
||||
if (m_measure_mode == EMeasureMode::ONLY_ASSEMBLY) {
|
||||
if (m_assembly_mode == AssemblyMode::FACE_FACE) {
|
||||
m_imgui->text(_u8L("Face") + " 2"+ _u8L(" (Moving)"));
|
||||
m_imgui->text(_u8L("Face 2 (Moving)"));
|
||||
} else if (m_assembly_mode == AssemblyMode::POINT_POINT) {
|
||||
m_imgui->text(_u8L("Point") + " 2"+ _u8L(" (Moving)"));
|
||||
m_imgui->text(_u8L("Point 2 (Moving)"));
|
||||
}
|
||||
} else {
|
||||
m_imgui->text(_u8L("Selection") + " 2");
|
||||
m_imgui->text(_u8L("Selection 2"));
|
||||
}
|
||||
ImGui::SameLine(selection_cap_length + space_size);
|
||||
ImGui::PushItemWidth(feature_second_text_length);
|
||||
|
||||
@@ -248,7 +248,7 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
|
||||
ImGui::CalcTextSize(_u8L("Subtract from").c_str()).x,
|
||||
ImGui::CalcTextSize(_u8L("Subtract with").c_str()).x});
|
||||
|
||||
const int select_btn_length = 2 * ImGui::GetStyle().FramePadding.x + std::max(ImGui::CalcTextSize(("1 " + _u8L("selected")).c_str()).x, ImGui::CalcTextSize(_u8L("Select").c_str()).x);
|
||||
const int select_btn_length = 2 * ImGui::GetStyle().FramePadding.x + std::max(ImGui::CalcTextSize(_u8L("1 selected").c_str()).x, ImGui::CalcTextSize(_u8L("Select").c_str()).x);
|
||||
|
||||
auto selectable = [](const std::string& label, bool selected, const ImVec2& size_arg) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 0,0 });
|
||||
@@ -323,7 +323,8 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
|
||||
std::string cap_str1 = m_operation_mode != MeshBooleanOperation::Difference ? _u8L("Part 1") : _u8L("Subtract from");
|
||||
m_imgui->text(cap_str1);
|
||||
ImGui::SameLine(max_cap_length);
|
||||
std::string select_src_str = m_src.mv ? "1 " + _u8L("selected") : _u8L("Select");
|
||||
// TRN Mesh Boolean button label once one volume is picked.
|
||||
std::string select_src_str = m_src.mv ? _u8L("1 selected") : _u8L("Select");
|
||||
select_src_str += "##select_source_volume";
|
||||
ImGui::PushItemWidth(select_btn_length);
|
||||
if (selectable(select_src_str, m_selecting_state == MeshBooleanSelectingState::SelectSource, ImVec2(select_btn_length, 0)))
|
||||
@@ -351,7 +352,7 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
|
||||
std::string cap_str2 = m_operation_mode != MeshBooleanOperation::Difference ? _u8L("Part 2") : _u8L("Subtract with");
|
||||
m_imgui->text(cap_str2);
|
||||
ImGui::SameLine(max_cap_length);
|
||||
std::string select_tool_str = m_tool.mv ? "1 " + _u8L("selected") : _u8L("Select");
|
||||
std::string select_tool_str = m_tool.mv ? _u8L("1 selected") : _u8L("Select");
|
||||
select_tool_str += "##select_tool_volume";
|
||||
ImGui::PushItemWidth(select_btn_length);
|
||||
if (selectable(select_tool_str, m_selecting_state == MeshBooleanSelectingState::SelectTool, ImVec2(select_btn_length, 0)))
|
||||
|
||||
@@ -56,8 +56,12 @@ static inline void show_notification_extruders_limit_exceeded()
|
||||
.plater()
|
||||
->get_notification_manager()
|
||||
->push_notification(NotificationType::MmSegmentationExceededExtrudersLimit, NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
|
||||
GUI::format(_L("Filament count exceeds the maximum number that painting tool supports. Only the "
|
||||
"first %1% filaments will be available in painting tool."), GLGizmoMmuSegmentation::EXTRUDERS_LIMIT));
|
||||
GUI::format(_L_PLURAL("Filament count exceeds the maximum number that painting tool supports. Only the "
|
||||
"first %1% filament will be available in painting tool.",
|
||||
"Filament count exceeds the maximum number that painting tool supports. Only the "
|
||||
"first %1% filaments will be available in painting tool.",
|
||||
unsigned(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)),
|
||||
GLGizmoMmuSegmentation::EXTRUDERS_LIMIT));
|
||||
}
|
||||
|
||||
void GLGizmoMmuSegmentation::on_opening()
|
||||
@@ -459,7 +463,7 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
|
||||
m_selected_extruder_idx = extruder_idx;
|
||||
}
|
||||
|
||||
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && 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(wxString::Format(_L("Shortcut Key %d"), extruder_idx + 1), max_tooltip_width);
|
||||
}
|
||||
// ORCA: Remap filaments section (Border only, Title in border).
|
||||
// Styled as a panel for visual grouping.
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoAlignment.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -109,6 +114,8 @@ void GLGizmoMove3D::on_set_state() {
|
||||
m_last_selected_volume_idx = -1;
|
||||
change_cs_by_selection();
|
||||
}
|
||||
if (m_object_manipulation)
|
||||
m_object_manipulation->reset_align_state();
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::on_start_dragging()
|
||||
@@ -157,6 +164,8 @@ void GLGizmoMove3D::on_render()
|
||||
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
|
||||
render_align_preview();
|
||||
|
||||
const auto &[box, box_trafo] = selection.get_bounding_box_in_current_reference_system();
|
||||
m_bounding_box = box;
|
||||
m_center = box_trafo.translation();
|
||||
@@ -340,6 +349,93 @@ double GLGizmoMove3D::calc_projection(const UpdateData& data) const
|
||||
return projection;
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::render_align_preview()
|
||||
{
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
if (m_object_manipulation == nullptr || m_object_manipulation->get_hovered_align_type() == AlignType::NONE)
|
||||
return;
|
||||
|
||||
// The box the hovered operation aligns to, in world coordinates like the operation itself.
|
||||
const AlignType type = m_object_manipulation->get_hovered_align_type();
|
||||
const GLGizmoAlignment &helper = m_object_manipulation->get_alignment_helper();
|
||||
const BoundingBoxf3 box = m_object_manipulation->get_align_to_parent() ? helper.get_parent_box() : helper.get_items_box();
|
||||
if (!box.defined)
|
||||
return;
|
||||
m_parent.get_selection().render_bounding_box(box, {0.0f, 150.0f / 255.0f, 136.0f / 255.0f}, m_parent.get_scale());
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
if (!m_align_plane.is_initialized()) {
|
||||
GLModel::Geometry init_data;
|
||||
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||||
init_data.reserve_vertices(4);
|
||||
init_data.reserve_indices(6);
|
||||
init_data.add_vertex(Vec3f(-0.5f, -0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(0.5f, -0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(0.5f, 0.5f, 0.0f));
|
||||
init_data.add_vertex(Vec3f(-0.5f, 0.5f, 0.0f));
|
||||
init_data.add_triangle(0, 1, 2);
|
||||
init_data.add_triangle(0, 2, 3);
|
||||
m_align_plane.init_from(std::move(init_data));
|
||||
m_align_plane.set_color({ 1.0f, 1.0f, 1.0f, 0.5f });
|
||||
}
|
||||
|
||||
// A plane across the box at its min, center or max on the hovered axis. Distributing shows up to six evenly
|
||||
// spaced planes.
|
||||
int axis = Z;
|
||||
double t = 0.5;
|
||||
switch (type) {
|
||||
case AlignType::X_MIN: axis = X; t = 0.0; break;
|
||||
case AlignType::X_MAX: axis = X; t = 1.0; break;
|
||||
case AlignType::CENTER_X:
|
||||
case AlignType::DISTRIBUTE_X: axis = X; break;
|
||||
case AlignType::Y_MIN: axis = Y; t = 0.0; break;
|
||||
case AlignType::Y_MAX: axis = Y; t = 1.0; break;
|
||||
case AlignType::CENTER_Y:
|
||||
case AlignType::DISTRIBUTE_Y: axis = Y; break;
|
||||
case AlignType::Z_MIN: t = 0.0; break;
|
||||
case AlignType::Z_MAX: t = 1.0; break;
|
||||
default: break;
|
||||
}
|
||||
const Vec3d size = box.size();
|
||||
std::vector<double> positions;
|
||||
if (type >= AlignType::DISTRIBUTE_X) {
|
||||
const size_t count = std::min<size_t>(helper.get_items_count(type, false), 6);
|
||||
for (size_t i = 1; i <= count; ++i)
|
||||
positions.push_back(box.min[axis] + i * size[axis] / (count + 1));
|
||||
} else
|
||||
positions.push_back(box.min[axis] + t * size[axis]);
|
||||
|
||||
const int u = (axis + 1) % 3;
|
||||
const int v = (axis + 2) % 3;
|
||||
Transform3d plane_matrix = Transform3d::Identity();
|
||||
plane_matrix.linear().col(0) = size[u] * Vec3d::Unit(u);
|
||||
plane_matrix.linear().col(1) = size[v] * Vec3d::Unit(v);
|
||||
plane_matrix.linear().col(2) = Vec3d::Unit(axis);
|
||||
Vec3d plane_center = box.center();
|
||||
|
||||
glsafe(::glDisable(GL_CULL_FACE));
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
// Translucent planes must hide neither each other nor the grabbers drawn after them.
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
shader->start_using();
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
for (double position : positions) {
|
||||
plane_center[axis] = position;
|
||||
plane_matrix.translation() = plane_center;
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * plane_matrix);
|
||||
m_align_plane.render();
|
||||
}
|
||||
shader->stop_using();
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
glsafe(::glEnable(GL_CULL_FACE));
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
void GLGizmoMove3D::change_cs_by_selection() {
|
||||
int obejct_idx, volume_idx;
|
||||
ModelVolume *model_volume = m_parent.get_selection().get_selected_single_volume(obejct_idx, volume_idx);
|
||||
|
||||
@@ -34,6 +34,8 @@ class GLGizmoMove3D : public GLGizmoBase
|
||||
Vec3d old_center{ Vec3d::Zero() };
|
||||
};
|
||||
std::array<GrabberConnection, 3> m_grabber_connections;
|
||||
// Unit square previewing the target plane of the hovered align or distribute icon.
|
||||
GLModel m_align_plane;
|
||||
|
||||
//BBS: add size adjust related
|
||||
GizmoObjectManipulation* m_object_manipulation;
|
||||
@@ -76,6 +78,7 @@ protected:
|
||||
|
||||
private:
|
||||
double calc_projection(const UpdateData& data) const;
|
||||
void render_align_preview();
|
||||
void change_cs_by_selection(); //cs mean Coordinate System
|
||||
private:
|
||||
int m_last_selected_obejct_idx, m_last_selected_volume_idx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GLGizmoSeam.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
//#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
@@ -327,7 +328,8 @@ void GLGizmoSeam::update_from_model_object(bool first_update)
|
||||
|
||||
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
|
||||
// Reset of TriangleSelector is done inside TriangleSelectorGUI's constructor, so we don't need it to perform it again in deserialize().
|
||||
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false);
|
||||
// The colour table has NONE/ENFORCER/BLOCKER only: a higher state in the file is reset to NONE.
|
||||
m_triangle_selectors.back()->deserialize(mv->seam_facets.get_data(), false, EnforcerBlockerType::BLOCKER);
|
||||
m_triangle_selectors.back()->request_update_render_data();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ void GLGizmoSimplify::on_render_input_window(float x, float y, float bottom_limi
|
||||
|
||||
ImGui::NewLine();
|
||||
ImGui::SameLine(bottom_left_width + space_size);
|
||||
ImGui::Text(_u8L("%d triangles").c_str(), m_configuration.wanted_count);
|
||||
ImGui::TextUnformatted(GUI::format(_L_PLURAL("%1% triangle", "%1% triangles", m_configuration.wanted_count), m_configuration.wanted_count).c_str());
|
||||
m_imgui->disabled_end(); // use_count
|
||||
|
||||
m_imgui->bbl_checkbox(_L("Show wireframe").c_str(), m_show_wireframe);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
#include "GLGizmoPainterBase.hpp"
|
||||
#include "libslic3r/TextureBake/TextureBakeDebug.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/AnimatedTexture.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLTexture.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
@@ -23,6 +24,7 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <imgui.h>
|
||||
#include <map>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
|
||||
@@ -61,6 +63,12 @@ public:
|
||||
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
|
||||
const TextureDisplacementFacetsData &masks,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
// `base`'s own UV channel, when it has one
|
||||
// (ModelVolume::texture_uv). Resampled onto
|
||||
// the result and reported back in it, and fed
|
||||
// to the samplers so a ModelUV layer drives
|
||||
// the refinement through its own layout.
|
||||
const std::vector<Vec2f> &model_uv,
|
||||
const TextureDisplacementPrepareParams ¶ms,
|
||||
const TextureColorSettings &color_settings,
|
||||
const DisplacementProgressFn &progress,
|
||||
@@ -435,6 +443,71 @@ private:
|
||||
std::string m_slider_type_request;
|
||||
int m_slider_type_request_frame = 0;
|
||||
|
||||
// ---- The image-adjustment panel ----
|
||||
//
|
||||
// Brightness/contrast/sharpness/blur/hue over one layer's image, shown on the image itself and
|
||||
// updating as the sliders move. What it produces is not a set of layer parameters: it replaces the
|
||||
// layer's *image*, so the adjusted pixels feed the colour and the displacement together and a
|
||||
// brightness change moves the relief too (see TextureAdjustments).
|
||||
//
|
||||
// An ordinary floating window rather than a popup, and deliberately so. A popup dims the canvas
|
||||
// behind it and closes on any click outside itself, which between them make it impossible to watch the
|
||||
// relief change on the model - which is the whole point of adjusting the image here rather than in an
|
||||
// image editor. A window leaves the 3D view live, clickable and orbitable while the panel is open.
|
||||
void render_image_adjust_window(const ImVec2 &panel_min, const ImVec2 &panel_max);
|
||||
// Opens the panel for `layer`, decoding its image into m_adjust_source. Sliders never re-decode
|
||||
// the PNG.
|
||||
void open_image_adjust(const TextureDisplacementLayer &layer);
|
||||
// Reads `layer`'s stored image into the panel's source buffers and records which image it came from.
|
||||
// Called when the panel opens and again whenever the layer's image is swapped under it, which is why
|
||||
// it leaves the slider values alone. False when the image cannot be decoded.
|
||||
bool load_image_adjust_source(const TextureDisplacementLayer &layer);
|
||||
// Resolves the panel: writes the adjusted image into the user's texture library as a new file and
|
||||
// points `layer` at it, which is the only way an adjustment becomes permanent. Closes the panel.
|
||||
void commit_image_adjust(TextureDisplacementLayer &layer);
|
||||
// Closes the panel and reverts the layer's live preview_adjust to identity, so every exit that is not
|
||||
// a commit - Cancel, the window's close button, the gizmo closing - leaves the layer as it was found.
|
||||
void close_image_adjust();
|
||||
// Collects a finished preview and starts one for the current slider values. One at a time, so a drag
|
||||
// coalesces into "whatever the sliders last said" instead of a queue of superseded frames.
|
||||
void update_image_adjust_preview();
|
||||
|
||||
int m_adjust_slot = -1; // the layer the panel is open for; -1 when closed
|
||||
TextureAdjustments m_adjust; // what the sliders currently say
|
||||
// What the layer's live preview_adjust was last set from, so a slider move can be told from a frame in
|
||||
// which nothing happened and only the former asks the displacement to be recomputed.
|
||||
TextureAdjustments m_adjust_applied;
|
||||
|
||||
// The layer's image as it was when the panel opened, and a small copy of it. The sliders are applied
|
||||
// to the small copy for the live preview and to the full-size one exactly once, on commit. Measured on
|
||||
// a 1024x1024 with every knob engaged, a full-size pass costs 13-19 ms grayscale and 40-60 ms in
|
||||
// colour (130-190 ms at 2048 in colour) - far too much to repeat per slider frame. The same pass on a
|
||||
// 384 px copy is 3-4 ms / 9-12 ms, and that is all the preview box can show anyway.
|
||||
DecodedHeightTexture m_adjust_source;
|
||||
DecodedHeightTexture m_adjust_preview_source;
|
||||
// Which image_data the two buffers above were decoded from, so the panel can notice the layer's image
|
||||
// being swapped from the texture picker while it is open and re-read it. Compared, never dereferenced.
|
||||
const void *m_adjust_source_image = nullptr;
|
||||
// And which volume the panel was opened on. Slot numbers are per volume, so without this a change of
|
||||
// selection would silently rebind the open panel to whichever layer happens to hold the same slot on
|
||||
// the newly selected object - a different image, under a panel the user opened for this one.
|
||||
size_t m_adjust_volume_id = 0;
|
||||
// The preview image on screen, re-uploaded whenever a background pass finishes.
|
||||
std::unique_ptr<GLTexture> m_adjust_preview_tex;
|
||||
|
||||
// The live preview runs on its own thread rather than on the UI thread or on the plater's job
|
||||
// worker. Not the UI thread because even the small pass is milliseconds and this runs while a slider
|
||||
// is being dragged; not the job worker because that is a single FIFO shared with Bake and the
|
||||
// displacement preview (see queue_preview_job()), where a per-frame image filter would sit behind -
|
||||
// and delay - multi-second mesh work.
|
||||
std::future<DecodedHeightTexture> m_adjust_preview_job;
|
||||
TextureAdjustments m_adjust_preview_running; // what the in-flight pass is computing
|
||||
TextureAdjustments m_adjust_preview_shown; // what m_adjust_preview_tex holds
|
||||
bool m_adjust_preview_valid = false;
|
||||
// Drops the GPU copies of the layer's image so the next frame rebuilds them from the decode, which
|
||||
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
|
||||
void invalidate_adjust_textures();
|
||||
|
||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
@@ -453,8 +526,8 @@ private:
|
||||
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
|
||||
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
|
||||
void render_paint_cursor_hint();
|
||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
|
||||
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
|
||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||
// on-canvas maps consistently onto offset whichever face the anchor is on.
|
||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||
|
||||
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
|
||||
@@ -855,18 +928,31 @@ private:
|
||||
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
|
||||
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
|
||||
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
|
||||
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
|
||||
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
|
||||
// the shader projects on its own. Shared by the shaded preview and the UV-check overlay.
|
||||
// unwrap), ModelUV (the model's own channel) and ViewProjected (a projector plane the shader
|
||||
// doesn't know). One entry per patch/base vertex, already through apply_uv_transform(). Empty for
|
||||
// Triplanar/Cylindrical/Spherical, which the shader projects on its own. Shared by the shaded
|
||||
// preview and the UV-check overlay.
|
||||
//
|
||||
// `patch_source` maps each patch triangle back to its base-mesh triangle
|
||||
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
|
||||
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
|
||||
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source = nullptr) const;
|
||||
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
|
||||
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
|
||||
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
|
||||
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
|
||||
// single-valued per point, so there a corner's UV is just its vertex's.
|
||||
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source = nullptr) const;
|
||||
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
|
||||
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
|
||||
// by the two functions above so the per-vertex and per-corner forms can never disagree.
|
||||
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer,
|
||||
const std::vector<int> *patch_source) const;
|
||||
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
|
||||
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
|
||||
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
|
||||
@@ -915,6 +1001,7 @@ private:
|
||||
enum class UVBackground { None, Height, Checker };
|
||||
UVBackground m_uv_editor_bg = UVBackground::None;
|
||||
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
|
||||
TextureAdjustments m_uv_editor_bg_adjust; // and the live image adjustment it was uploaded at
|
||||
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
|
||||
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
|
||||
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
|
||||
@@ -1069,6 +1156,50 @@ private:
|
||||
std::map<std::string, IconManager::Icons> m_panel_icon_map; // file name -> [normal, colour, disabled]
|
||||
bool m_panel_icons_tried = false;
|
||||
void ensure_panel_icons();
|
||||
|
||||
// One illustration under a tooltip's text: a still image or an animation (see AnimatedTexture),
|
||||
// named by file within resources/images/texture_displacement, with a caption under it. Several of
|
||||
// them in one tooltip are laid out side by side, which is how a setting is shown as a comparison -
|
||||
// the same model at two values of it.
|
||||
struct TipMedia
|
||||
{
|
||||
std::string file;
|
||||
wxString caption;
|
||||
};
|
||||
|
||||
// Every tooltip the panel shows. Placed beside the panel rather than under the mouse: the panel is
|
||||
// pinned to the right edge of the canvas, so a tooltip left to follow the mouse covers the control
|
||||
// it is explaining, and one carrying an illustration covers most of the panel with it.
|
||||
//
|
||||
// `media` is drawn under the text, at the tooltip's full width; files that are missing or will not
|
||||
// decode are skipped, so a tooltip whose illustration has not been drawn yet is exactly the text
|
||||
// tooltip it was before.
|
||||
void panel_tooltip(const std::string &text, const std::vector<TipMedia> &media = {});
|
||||
void panel_tooltip(const wxString &text, const std::vector<TipMedia> &media = {});
|
||||
void render_tip_media(const std::vector<TipMedia> &media, float width);
|
||||
|
||||
// Screen rectangle a tooltip places itself beside: the panel window, or - while the texture picker
|
||||
// is up and tooltips are coming from inside it - the picker popup, which already sits beside the
|
||||
// panel. Both are set from the window's own rectangle as it is drawn.
|
||||
ImVec2 m_tooltip_anchor_min{0.f, 0.f};
|
||||
ImVec2 m_tooltip_anchor_max{0.f, 0.f};
|
||||
// Illustrations any tooltip has shown, decoded and uploaded once (see AnimatedTexture.hpp).
|
||||
AnimatedTextureCache m_tip_media;
|
||||
// Which tooltip's illustrations are on screen, and when they first appeared: an animation plays
|
||||
// from its first frame each time its tooltip opens, rather than from wherever a free-running clock
|
||||
// happened to be.
|
||||
std::string m_tip_media_key;
|
||||
double m_tip_media_start = 0.;
|
||||
int m_tip_media_frame = -2; // the frame it was last drawn on, to spot the tooltip reopening
|
||||
// How long the pointer has rested on the control a tooltip belongs to, and when one was last on
|
||||
// screen, both on the real clock tip_now() keeps (see the .cpp for why ImGui's own will not do).
|
||||
ImVec2 m_tip_item{-1.f, -1.f};
|
||||
double m_tip_hover_since = 0.;
|
||||
double m_tip_last_shown = -1e9;
|
||||
// The frame a tooltip was last drawn on, and how many it has been drawn on in a row: ImGui hides
|
||||
// one on its first frame while it measures it, so it has to be kept on screen to be seen at all.
|
||||
int m_tip_shown_frame = -2;
|
||||
int m_tip_settled = 0;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
@@ -373,6 +373,25 @@ bool GLGizmosManager::init_icon_textures()
|
||||
icon_list.insert(std::make_pair((int) icon, texture_id));
|
||||
}
|
||||
|
||||
for (const auto& [icon, name] : std::initializer_list<std::pair<MENU_ICON_NAME, const char*>>{
|
||||
{ IC_ALIGN_X_MIN , "align_x_min" }, { IC_ALIGN_X_MIN_DARK , "align_x_min_dark" },
|
||||
{ IC_ALIGN_X_CENTER, "align_x_center" }, { IC_ALIGN_X_CENTER_DARK, "align_x_center_dark" },
|
||||
{ IC_ALIGN_X_MAX , "align_x_max" }, { IC_ALIGN_X_MAX_DARK , "align_x_max_dark" },
|
||||
{ IC_ALIGN_Y_MIN , "align_y_min" }, { IC_ALIGN_Y_MIN_DARK , "align_y_min_dark" },
|
||||
{ IC_ALIGN_Y_CENTER, "align_y_center" }, { IC_ALIGN_Y_CENTER_DARK, "align_y_center_dark" },
|
||||
{ IC_ALIGN_Y_MAX , "align_y_max" }, { IC_ALIGN_Y_MAX_DARK , "align_y_max_dark" },
|
||||
{ IC_ALIGN_Z_MIN , "align_z_min" }, { IC_ALIGN_Z_MIN_DARK , "align_z_min_dark" },
|
||||
{ IC_ALIGN_Z_CENTER, "align_z_center" }, { IC_ALIGN_Z_CENTER_DARK, "align_z_center_dark" },
|
||||
{ IC_ALIGN_Z_MAX , "align_z_max" }, { IC_ALIGN_Z_MAX_DARK , "align_z_max_dark" },
|
||||
{ IC_DISTRIBUTE_X , "distribute_x" }, { IC_DISTRIBUTE_X_DARK , "distribute_x_dark" },
|
||||
{ IC_DISTRIBUTE_Y , "distribute_y" }, { IC_DISTRIBUTE_Y_DARK , "distribute_y_dark" },
|
||||
{ IC_DISTRIBUTE_Z , "distribute_z" }, { IC_DISTRIBUTE_Z_DARK , "distribute_z_dark" }
|
||||
}) {
|
||||
if (!IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/" + name + ".svg", 16, 16, texture_id))
|
||||
return false;
|
||||
icon_list.insert(std::make_pair((int) icon, texture_id));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -617,6 +636,13 @@ bool GLGizmosManager::is_allow_select_all() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ctrl+click and Alt+click grow the selection only where that serves the gizmo: the Move gizmo aligns and distributes
|
||||
// several objects or parts, while Scale uses Ctrl for single sided scaling.
|
||||
bool GLGizmosManager::is_allow_multi_select_parts_or_objects() const
|
||||
{
|
||||
return m_current == Undefined || m_current == EType::Move;
|
||||
}
|
||||
|
||||
std::optional<ClippingPlane> GLGizmosManager::get_clipping_plane() const
|
||||
{
|
||||
if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper())
|
||||
|
||||
@@ -201,6 +201,30 @@ public:
|
||||
IC_CANVAS_SECTION_ACTIVE_HOVER,
|
||||
IC_CANVAS_SECTION_ACTIVE_DARK,
|
||||
IC_CANVAS_SECTION_ACTIVE_DARK_HOVER,
|
||||
IC_ALIGN_X_MIN,
|
||||
IC_ALIGN_X_CENTER,
|
||||
IC_ALIGN_X_MAX,
|
||||
IC_ALIGN_Y_MIN,
|
||||
IC_ALIGN_Y_CENTER,
|
||||
IC_ALIGN_Y_MAX,
|
||||
IC_ALIGN_Z_MIN,
|
||||
IC_ALIGN_Z_CENTER,
|
||||
IC_ALIGN_Z_MAX,
|
||||
IC_DISTRIBUTE_X,
|
||||
IC_DISTRIBUTE_Y,
|
||||
IC_DISTRIBUTE_Z,
|
||||
IC_ALIGN_X_MIN_DARK,
|
||||
IC_ALIGN_X_CENTER_DARK,
|
||||
IC_ALIGN_X_MAX_DARK,
|
||||
IC_ALIGN_Y_MIN_DARK,
|
||||
IC_ALIGN_Y_CENTER_DARK,
|
||||
IC_ALIGN_Y_MAX_DARK,
|
||||
IC_ALIGN_Z_MIN_DARK,
|
||||
IC_ALIGN_Z_CENTER_DARK,
|
||||
IC_ALIGN_Z_MAX_DARK,
|
||||
IC_DISTRIBUTE_X_DARK,
|
||||
IC_DISTRIBUTE_Y_DARK,
|
||||
IC_DISTRIBUTE_Z_DARK,
|
||||
};
|
||||
|
||||
explicit GLGizmosManager(GLCanvas3D& parent);
|
||||
@@ -301,6 +325,7 @@ public:
|
||||
|
||||
bool is_paint_gizmo();
|
||||
bool is_allow_select_all();
|
||||
bool is_allow_multi_select_parts_or_objects() const;
|
||||
// Empty when the open gizmo has no object clipper.
|
||||
std::optional<ClippingPlane> get_clipping_plane() const;
|
||||
bool wants_reslice_supports_on_undo() const;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
//#include "I18N.hpp"
|
||||
#include "GLGizmosManager.hpp"
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
@@ -56,26 +57,8 @@ const double GizmoObjectManipulation::mm_to_in = 0.0393700787;
|
||||
const double GizmoObjectManipulation::oz_to_g = 28.34952;
|
||||
const double GizmoObjectManipulation::g_to_oz = 0.035274;
|
||||
|
||||
// Helper function to be used by drop to bed button. Returns lowest point of this
|
||||
// volume in world coordinate system.
|
||||
static double get_volume_min_z(const GLVolume* volume)
|
||||
{
|
||||
const Transform3f& world_matrix = volume->world_matrix().cast<float>();
|
||||
|
||||
// need to get the ModelVolume pointer
|
||||
const ModelObject* mo = wxGetApp().model().objects[volume->composite_id.object_id];
|
||||
const ModelVolume* mv = mo->volumes[volume->composite_id.volume_id];
|
||||
const TriangleMesh& hull = mv->get_convex_hull();
|
||||
|
||||
float min_z = std::numeric_limits<float>::max();
|
||||
for (const stl_vertex& vert : hull.its.vertices) {
|
||||
min_z = std::min(min_z, Vec3f::UnitZ().dot(world_matrix * vert));
|
||||
}
|
||||
return min_z;
|
||||
}
|
||||
|
||||
GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
: m_glcanvas(glcanvas)
|
||||
: m_glcanvas(glcanvas), m_alignment_helper(glcanvas)
|
||||
{
|
||||
m_imperial_units = wxGetApp().app_config->get("use_inches") == "1";
|
||||
m_new_unit_string = m_imperial_units ? L_CONTEXT("in", "inches") : L("mm");
|
||||
@@ -86,6 +69,7 @@ GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas)
|
||||
|
||||
m_shortcuts_move = {
|
||||
{alt + _L("Left mouse button"), _L("Part selection")},
|
||||
{ctrl + _L("Left mouse button"), _L("Select multiple objects")},
|
||||
{shift + _L("Left mouse button"), _L("Fixed step drag")},
|
||||
{_L("Context Menu"), _L("Toggle Auto-Drop")}
|
||||
};
|
||||
@@ -293,12 +277,14 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
{
|
||||
const Selection& selection = m_glcanvas.get_selection();
|
||||
|
||||
// Assembly view has its own selection and transformations, which drop_selection() doesn't update
|
||||
m_enable_drop_to_bed = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D && selection.can_drop();
|
||||
|
||||
if (selection.is_single_full_instance() || selection.is_single_volume_or_modifier()) {
|
||||
const GLVolume * volume = selection.get_first_volume();
|
||||
|
||||
Vec3d rotation;
|
||||
Vec3d scale;
|
||||
double min_z = 0.;
|
||||
|
||||
if (selection.is_single_full_instance()) {
|
||||
rotation = volume->get_instance_rotation();
|
||||
@@ -307,12 +293,10 @@ void GizmoObjectManipulation::update_reset_buttons_visibility()
|
||||
else {
|
||||
rotation = volume->get_volume_rotation();
|
||||
scale = volume->get_volume_scaling_factor();
|
||||
min_z = get_volume_min_z(volume);
|
||||
}
|
||||
m_show_clear_rotation = !rotation.isApprox(m_init_rotation);
|
||||
m_show_reset_0_rotation = !rotation.isApprox(Vec3d::Zero());
|
||||
m_show_clear_scale = (m_cache.scale / 100.0f - Vec3d::Ones()).norm() > 0.001;
|
||||
m_show_drop_to_bed = (std::abs(min_z) > EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,8 +745,19 @@ void GizmoObjectManipulation::set_init_rotation(const Geometry::Transformation &
|
||||
m_init_rotation = value.get_rotation();
|
||||
}
|
||||
|
||||
void GizmoObjectManipulation::reset_align_state()
|
||||
{
|
||||
m_hovered_align_type = GLGizmoAlignment::AlignType::NONE;
|
||||
m_align_to_parent = !m_alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN);
|
||||
}
|
||||
|
||||
void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit)
|
||||
{
|
||||
float scale = m_glcanvas.get_scale();
|
||||
#ifdef WIN32
|
||||
scale *= (float)get_dpi_for_window(wxGetApp().GetTopWindow()) / (float)DPI_DEFAULT;
|
||||
#endif // WIN32
|
||||
|
||||
// BBS: GUI refactor: move gizmo to the right
|
||||
if (abs(last_move_input_window_width) > 0.01f) {
|
||||
if (x + last_move_input_window_width > m_glcanvas.get_canvas_size().get_width()) {
|
||||
@@ -782,6 +777,8 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
ImGuiWrapper::push_toolbar_style(m_glcanvas.get_scale());
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0, 6.0));
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
std::string name = this->m_new_title_string + "##" + window_name;
|
||||
imgui_wrapper->begin(_L(name), ImGuiWrapper::TOOLBAR_WINDOW_FLAGS);
|
||||
|
||||
@@ -798,16 +795,26 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
};
|
||||
|
||||
float space_size = imgui_wrapper->get_style_scaling() * 8;
|
||||
// The align section shares the combo width, the caption column and the input columns of the position row.
|
||||
const bool show_align = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D &&
|
||||
!m_glcanvas.get_selection().is_wipe_tower();
|
||||
//ORCA
|
||||
float coord_combo_width = std::max({
|
||||
imgui_wrapper->calc_text_size(_L("World")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Object")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Part")).x
|
||||
}) + imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
|
||||
});
|
||||
float label_max = std::max({
|
||||
imgui_wrapper->calc_text_size(_L("Position")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Relative")).x
|
||||
});
|
||||
if (show_align) {
|
||||
coord_combo_width = std::max({coord_combo_width, imgui_wrapper->calc_text_size(_L("Selection")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Object")).x,
|
||||
imgui_wrapper->calc_text_size(_L("Plate")).x});
|
||||
label_max = std::max(label_max, imgui_wrapper->calc_text_size(_L("Distribute")).x);
|
||||
}
|
||||
coord_combo_width += imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f);
|
||||
float caption_max = std::max(label_max, coord_combo_width - 3 * space_size);
|
||||
float end_text_size = imgui_wrapper->calc_text_size(this->m_new_unit_string).x;
|
||||
|
||||
@@ -821,6 +828,13 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
// Rotation
|
||||
float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size;
|
||||
// Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them.
|
||||
const float align_icon_size = 16.f * scale;
|
||||
const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA
|
||||
if (show_align) {
|
||||
unit_size = std::max(unit_size, 3.f * align_button_size);
|
||||
unit_size = std::round(unit_size); // ORCA keep columns on whole pixels
|
||||
}
|
||||
int index = 1;
|
||||
int index_unit = 1;
|
||||
|
||||
@@ -909,6 +923,134 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
}
|
||||
if (!focued_on_text) m_glcanvas.handle_sidebar_focus_event("", false);
|
||||
|
||||
// Align and distribute along the world axes, within the selection or to the object or plate holding it.
|
||||
using AlignType = GLGizmoAlignment::AlignType;
|
||||
const bool old_align_to_parent = m_align_to_parent;
|
||||
AlignType hovered_align_type = AlignType::NONE;
|
||||
if (show_align) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Each row is row_height tall, separated by ItemSpacing.y. Every control is centered vertically in its row.
|
||||
const float row_y = ImGui::GetCursorPosY();
|
||||
const float button_height = align_icon_size + 2.f * style.FramePadding.y;
|
||||
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
|
||||
const float combo_height = ImGui::GetFrameHeight();
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
const float row_height = std::max({ImGui::GetTextLineHeight(), button_height, combo_height});
|
||||
const float row_gap = style.ItemSpacing.y;
|
||||
int row = 0;
|
||||
auto row_top = [&](int r) { return row_y + r * (row_height + row_gap); };
|
||||
// Snap to whole pixels to avoid blurry icons
|
||||
auto center_in_row = [&](int r, float item_height) { return std::floor(row_top(r) + (row_height - item_height) * 0.5f); };
|
||||
|
||||
// Row 0: align mode combo in the caption column.
|
||||
ImGui::SetCursorPosY(center_in_row(row, combo_height));
|
||||
|
||||
// The last mode aligns to the parent. Aligning within the selection needs two items.
|
||||
std::vector<std::string> align_modes = {
|
||||
_u8L("Selection"),
|
||||
m_alignment_helper.is_parts_selection(true) ? _u8L("Object") : _u8L("Plate")
|
||||
};
|
||||
if (!m_alignment_helper.can_align(AlignType::X_MIN)) {
|
||||
align_modes.erase(align_modes.begin());
|
||||
m_align_to_parent = true;
|
||||
}
|
||||
size_t align_mode_idx = m_align_to_parent ? align_modes.size() - 1 : 0;
|
||||
ImGuiWrapper::push_combo_style(m_glcanvas.get_scale());
|
||||
// render_combo() takes its ImGui id from the empty label, which the coordinate combo uses too.
|
||||
ImGui::PushID("align_mode");
|
||||
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
|
||||
m_align_to_parent = align_mode_idx + 1 == align_modes.size();
|
||||
ImGui::PopID();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString tooltip_str = _L("Reference for alignment");
|
||||
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
const bool dark_mode = wxGetApp().dark_mode();
|
||||
int current_axis = -1;
|
||||
auto align_icon = [&](int axis, AlignType type, GLGizmosManager::MENU_ICON_NAME icon,
|
||||
GLGizmosManager::MENU_ICON_NAME dark_icon, const wxString &tip) {
|
||||
const bool distribute = type >= AlignType::DISTRIBUTE_X;
|
||||
const bool enabled = m_align_to_parent ? m_alignment_helper.can_align(type, true) :
|
||||
distribute ? m_alignment_helper.can_distribute(type) :
|
||||
m_alignment_helper.can_align(type);
|
||||
if (axis != current_axis) {
|
||||
ImGui::SameLine(int(caption_max + (axis + 1) * space_size + axis * unit_size)); // ORCA ensure placement snaps to pixel to fix blur
|
||||
if (current_axis < 0)
|
||||
ImGui::SetCursorPosY(center_in_row(row, button_height));
|
||||
current_axis = axis;
|
||||
} else
|
||||
ImGui::SameLine(0, 0);
|
||||
|
||||
if (!enabled) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3f);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]);
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_Button]);
|
||||
}
|
||||
ImTextureID texture = m_glcanvas.get_gizmos_manager().get_icon_texture_id(dark_mode ? dark_icon : icon);
|
||||
if (ImGui::ImageButton3(texture, texture, ImVec2(align_icon_size, align_icon_size)) && enabled) {
|
||||
if (distribute)
|
||||
m_alignment_helper.distribute_objects(type);
|
||||
else
|
||||
m_alignment_helper.align_objects(type, m_align_to_parent);
|
||||
}
|
||||
if (!enabled) {
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString text = tip;
|
||||
if (enabled)
|
||||
hovered_align_type = type;
|
||||
else {
|
||||
// Too few items is the reason when there are items to move (only distribute needs three);
|
||||
// otherwise the helper names it.
|
||||
std::string why_not;
|
||||
if (m_alignment_helper.get_items_count(type, m_align_to_parent, &why_not) > 0)
|
||||
why_not = _u8L("Select at least 3 parts or objects");
|
||||
if (!why_not.empty())
|
||||
text += "\n" + from_u8(why_not);
|
||||
}
|
||||
imgui_wrapper->tooltip(text, imgui_wrapper->calc_text_size(text).x + 3 * space_size);
|
||||
}
|
||||
};
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
align_icon(0, AlignType::X_MIN, GLGizmosManager::IC_ALIGN_X_MIN, GLGizmosManager::IC_ALIGN_X_MIN_DARK, _L("Align left") + " (-X)");
|
||||
align_icon(0, AlignType::CENTER_X, GLGizmosManager::IC_ALIGN_X_CENTER, GLGizmosManager::IC_ALIGN_X_CENTER_DARK, _L("Align left-right center") + " (X)");
|
||||
align_icon(0, AlignType::X_MAX, GLGizmosManager::IC_ALIGN_X_MAX, GLGizmosManager::IC_ALIGN_X_MAX_DARK, _L("Align right") + " (+X)");
|
||||
|
||||
align_icon(1, AlignType::Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN_DARK, _L("Align front") + " (-Y)");
|
||||
align_icon(1, AlignType::CENTER_Y, GLGizmosManager::IC_ALIGN_Y_CENTER, GLGizmosManager::IC_ALIGN_Y_CENTER_DARK, _L("Align front-back center") + " (Y)");
|
||||
align_icon(1, AlignType::Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX_DARK, _L("Align back") + " (+Y)");
|
||||
|
||||
align_icon(2, AlignType::Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN_DARK, _L("Align bottom") + " (-Z)");
|
||||
align_icon(2, AlignType::CENTER_Z, GLGizmosManager::IC_ALIGN_Z_CENTER, GLGizmosManager::IC_ALIGN_Z_CENTER_DARK, _L("Align top-bottom center") + " (Z)");
|
||||
align_icon(2, AlignType::Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX_DARK, _L("Align top") + " (+Z)");
|
||||
|
||||
if (!m_align_to_parent) {
|
||||
row = 1;
|
||||
current_axis = -1;
|
||||
ImGui::SetCursorPosY(center_in_row(row, ImGui::GetTextLineHeight()));
|
||||
imgui_wrapper->text(_L("Distribute"));
|
||||
align_icon(0, AlignType::DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X_DARK, _L("Distribute left-right") + " (X)");
|
||||
align_icon(1, AlignType::DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y_DARK, _L("Distribute front-back") + " (Y)");
|
||||
align_icon(2, AlignType::DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z_DARK, _L("Distribute top-bottom") + " (Z)");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::SetCursorPosY(row_top(row + 1));
|
||||
}
|
||||
// The Move gizmo draws the preview with the scene, which an ImGui-only frame does not redraw.
|
||||
if (hovered_align_type != m_hovered_align_type || m_align_to_parent != old_align_to_parent) {
|
||||
m_hovered_align_type = hovered_align_type;
|
||||
m_glcanvas.set_as_dirty();
|
||||
m_glcanvas.request_extra_frame();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
@@ -917,6 +1059,17 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
|
||||
GLGizmoUtils::render_tooltip_button(imgui_wrapper, m_glcanvas, m_shortcuts_move, x, y);
|
||||
|
||||
// ORCA use existing function for drop and show button on bottom bar to keep gizmo compact
|
||||
imgui_wrapper->disabled_begin(!m_enable_drop_to_bed);
|
||||
ImGui::SameLine(0, space_size * 1.5f);
|
||||
if (imgui_wrapper->button(_L("Drop"))){
|
||||
wxGetApp().plater()->drop_selection();
|
||||
UpdateAndShow(true);
|
||||
}
|
||||
imgui_wrapper->disabled_end();
|
||||
if(ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
imgui_wrapper->tooltip(_L("Move the selection along Z so that its lowest point touches the bed."), ImGui::GetFontSize() * 20.0f);
|
||||
|
||||
ImGui::SameLine();
|
||||
GLGizmoUtils::begin_right_aligned_buttons({ _L("Done") });
|
||||
if (imgui_wrapper->button(_L("Done"))) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "slic3r/GUI/GUI_Geometry.hpp"
|
||||
#include "GLGizmoAlignment.hpp"
|
||||
|
||||
//#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
|
||||
@@ -102,7 +103,7 @@ public:
|
||||
bool m_show_reset_0_rotation{false};
|
||||
bool m_show_clear_rotation { false };
|
||||
bool m_show_clear_scale { false };
|
||||
bool m_show_drop_to_bed { false };
|
||||
bool m_enable_drop_to_bed { false };
|
||||
enum class RotateType { None, Relative, Absolute
|
||||
};
|
||||
RotateType m_last_rotate_type{RotateType::None}; // 0:no input 1:relative 2:absolute
|
||||
@@ -151,6 +152,13 @@ public:
|
||||
|
||||
void set_init_rotation(const Geometry::Transformation &value);
|
||||
|
||||
// Align/distribute state of the Move panel, which the Move gizmo previews.
|
||||
const GLGizmoAlignment &get_alignment_helper() const { return m_alignment_helper; }
|
||||
GLGizmoAlignment::AlignType get_hovered_align_type() const { return m_hovered_align_type; }
|
||||
bool get_align_to_parent() const { return m_align_to_parent; }
|
||||
// When the Move gizmo opens or closes: no preview, and "Align selected" whenever the selection allows it.
|
||||
void reset_align_state();
|
||||
|
||||
private:
|
||||
void reset_settings_value();
|
||||
void update_settings_value(const Selection& selection);
|
||||
@@ -175,6 +183,12 @@ private:
|
||||
GLCanvas3D& m_glcanvas;
|
||||
unsigned int m_last_active_item { 0 };
|
||||
|
||||
GLGizmoAlignment m_alignment_helper;
|
||||
// Operation of the enabled align icon under the mouse.
|
||||
GLGizmoAlignment::AlignType m_hovered_align_type{GLGizmoAlignment::AlignType::NONE};
|
||||
// Align to the object or plate holding the selection rather than within the selection.
|
||||
bool m_align_to_parent{false};
|
||||
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
std::vector<std::pair<wxString, wxString>> m_shortcuts_move;
|
||||
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "slic3r/GUI/BitmapComboBox.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
@@ -95,7 +96,7 @@ void IMEXFilamentPickerPopover::build_row()
|
||||
};
|
||||
|
||||
for (int L : lane_logicals) {
|
||||
const wxString label = wxString::Format("filament %d", L + 1);
|
||||
const wxString label = wxString::Format(_L("Filament %d"), L + 1);
|
||||
if (L >= 0 && L < (int)bmps.size() && bmps[L] && bmps[L]->IsOk())
|
||||
choice->Append(label, pad_left(*bmps[L]));
|
||||
else
|
||||
|
||||
@@ -321,15 +321,21 @@ bool IMEXModesCtrl::fix_row_name(Row& r) {
|
||||
IMEXModesCtrl::RoleStyle IMEXModesCtrl::role_style(std::optional<ImexRole> role) {
|
||||
// Gray / "Inactive" is the no-role answer; every role gets an explicit case so a new
|
||||
// one is a compile-time -Wswitch prompt rather than a tile that silently renders gray.
|
||||
// TRN IDEX/IQEX role of a tool head that takes no part in the mode.
|
||||
const wxString inactive = _L_CONTEXT("Inactive", "IDEX/IQEX role");
|
||||
if (!role)
|
||||
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" };
|
||||
return { wxColour(90, 90, 90), *wxWHITE, inactive };
|
||||
switch (*role) {
|
||||
case ImexRole::Primary: return { wxColour(50, 160, 50), *wxWHITE, "Primary" }; // green
|
||||
case ImexRole::Copy: return { wxColour(60, 120, 210), *wxWHITE, "Copy" }; // blue
|
||||
case ImexRole::Mirror: return { wxColour(210, 130, 20), *wxWHITE, "Mirror" }; // amber
|
||||
case ImexRole::Span: return { wxColour(180, 180, 40), *wxWHITE, "Span" }; // yellow
|
||||
// TRN IDEX/IQEX role: the tool head the slicer generates paths for.
|
||||
case ImexRole::Primary: return { wxColour(50, 160, 50), *wxWHITE, _L_CONTEXT("Primary", "IDEX/IQEX role") }; // green
|
||||
// TRN IDEX/IQEX role: the tool head prints a copy of the Primary's paths.
|
||||
case ImexRole::Copy: return { wxColour(60, 120, 210), *wxWHITE, _L_CONTEXT("Copy", "IDEX/IQEX role") }; // blue
|
||||
// TRN IDEX/IQEX role: the tool head prints a mirror image of the Primary's paths.
|
||||
case ImexRole::Mirror: return { wxColour(210, 130, 20), *wxWHITE, _L_CONTEXT("Mirror", "IDEX/IQEX role") }; // amber
|
||||
// TRN IDEX/IQEX role: multicolor partner of the Primary on the same gantry.
|
||||
case ImexRole::Span: return { wxColour(180, 180, 40), *wxWHITE, _L_CONTEXT("Span", "IDEX/IQEX role") }; // yellow
|
||||
}
|
||||
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" }; // keeps every compiler quiet
|
||||
return { wxColour(90, 90, 90), *wxWHITE, inactive }; // keeps every compiler quiet
|
||||
}
|
||||
|
||||
wxString IMEXModesCtrl::role_hint(ImexRole role) {
|
||||
|
||||
@@ -147,9 +147,8 @@ private:
|
||||
|
||||
// Everything the editor draws for one role, in ONE place: a new role needs a color and
|
||||
// a legend name here and nowhere else in this file. nullopt is Inactive (gray).
|
||||
// `label` is deliberately untranslated — it names a role, and the legend sits next to
|
||||
// the same letters the preset stores.
|
||||
struct RoleStyle { wxColour bg; wxColour fg; const char* label; };
|
||||
// `label` is display-only and translated; the preset stores the role letters, never it.
|
||||
struct RoleStyle { wxColour bg; wxColour fg; wxString label; };
|
||||
static RoleStyle role_style(std::optional<ImexRole> role);
|
||||
|
||||
// True when this printer offers `role` at all, regardless of which tile is clicked.
|
||||
|
||||
@@ -1564,7 +1564,7 @@ void IMSlider::render_add_menu()
|
||||
ImU32 icon_clr = ImGuiWrapper::to_ImU32(rgba);
|
||||
if (rgba.a() == 0)
|
||||
icon_clr = 0;
|
||||
if (menu_item_with_icon((_u8L("Filament ") + std::to_string(i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr, false, true, &hovered)) add_code_as_tick(ToolChange, i + 1);
|
||||
if (menu_item_with_icon(into_u8(wxString::Format(_L("Filament %d"), i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr, false, true, &hovered)) add_code_as_tick(ToolChange, i + 1);
|
||||
if (hovered) { show_tooltip(_u8L("Change filament at the beginning of this layer.")); }
|
||||
}
|
||||
end_menu();
|
||||
@@ -1614,7 +1614,7 @@ void IMSlider::render_edit_menu(const TickCode& tick)
|
||||
for (int i = 0; i < extruder_num; i++) {
|
||||
ColorRGBA rgba = decode_color_to_float_array(m_extruder_colors[i]);
|
||||
ImU32 icon_clr = ImGuiWrapper::to_ImU32(rgba);
|
||||
if (menu_item_with_icon((_u8L("Filament ") + std::to_string(i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr)) add_code_as_tick(ToolChange, i + 1);
|
||||
if (menu_item_with_icon(into_u8(wxString::Format(_L("Filament %d"), i + 1)).c_str(), "", ImVec2(14, 14) * m_scale, icon_clr)) add_code_as_tick(ToolChange, i + 1);
|
||||
}
|
||||
end_menu();
|
||||
}
|
||||
|
||||
@@ -23,27 +23,23 @@ IMToolbarItem::~IMToolbarItem()
|
||||
}
|
||||
|
||||
|
||||
bool IMToolbarItem::generate_texture()
|
||||
void IMToolbarItem::set_thumbnail(const ThumbnailData& thumbnail)
|
||||
{
|
||||
GLint last_texture;
|
||||
unsigned m_image_texture{ 0 };
|
||||
unsigned char* pixels = (unsigned char*)(&image_data[0]);
|
||||
if (!thumbnail.is_valid())
|
||||
return;
|
||||
|
||||
GLint last_texture;
|
||||
GLuint id = (GLuint)(intptr_t)texture_id;
|
||||
glsafe(::glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture));
|
||||
glsafe(::glGenTextures(1, &m_image_texture));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, m_image_texture));
|
||||
if (id == 0)
|
||||
glsafe(::glGenTextures(1, &id));
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, id));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
|
||||
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
|
||||
glsafe(::glPixelStorei(GL_UNPACK_ROW_LENGTH, 0));
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
|
||||
|
||||
// Store our identifier
|
||||
texture_id = (ImTextureID)(intptr_t)m_image_texture;
|
||||
|
||||
// Restore state
|
||||
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, thumbnail.width, thumbnail.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, thumbnail.pixels.data()));
|
||||
texture_id = (ImTextureID)(intptr_t)id;
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, last_texture));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IMToolbar::del_all_item()
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define DEFAULT_TOOLBAR_BUTTON_HEIGHT 80.0f
|
||||
|
||||
namespace Slic3r {
|
||||
struct ThumbnailData;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class IMToolbarItem
|
||||
@@ -33,11 +35,9 @@ public:
|
||||
SliceState slice_state;
|
||||
|
||||
ImTextureID texture_id { 0 };
|
||||
std::vector<unsigned char> image_data;
|
||||
unsigned int image_width;
|
||||
unsigned int image_height;
|
||||
|
||||
bool generate_texture();
|
||||
// Uploads a valid thumbnail as the texture; the item keeps no copy of its pixels.
|
||||
void set_thumbnail(const ThumbnailData& thumbnail);
|
||||
~IMToolbarItem();
|
||||
};
|
||||
|
||||
|
||||
@@ -647,7 +647,7 @@ void ArrangeJob::check_unprintable()
|
||||
if (it->poly.area() < 0.001) {
|
||||
auto msg = (boost::format(
|
||||
_u8L("Object %s has zero size and can't be arranged."))
|
||||
% _utf8(it->name)).str();
|
||||
% it->name).str();
|
||||
m_plater->get_notification_manager()->push_notification(NotificationType::BBLPlateInfo,
|
||||
NotificationManager::NotificationLevel::WarningNotificationLevel, msg);
|
||||
}
|
||||
@@ -693,7 +693,8 @@ void ArrangeJob::process(Ctl &ctl)
|
||||
params.stopcondition = [&ctl]() { return ctl.was_canceled(); };
|
||||
|
||||
params.progressind = [this, &ctl](unsigned num_finished, std::string str = "") {
|
||||
ctl.update_status(num_finished * 100 / status_range(), _u8L("Arranging ") + str);
|
||||
// TRN Progress status. %1% is the name of the object being arranged.
|
||||
ctl.update_status(num_finished * 100 / status_range(), GUI::format(_u8L("Arranging %1%"), str));
|
||||
};
|
||||
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
|
||||
@@ -265,9 +266,10 @@ void FillBedJob::process(Ctl &ctl)
|
||||
return ctl.was_canceled() || do_stop;
|
||||
};
|
||||
|
||||
params.progressind = [this, &ctl, &statustxt](unsigned st,std::string str="") {
|
||||
params.progressind = [this, &ctl](unsigned st,std::string str="") {
|
||||
if (st > 0)
|
||||
ctl.update_status(st * 100 / status_range(), statustxt + " " + str);
|
||||
// TRN Progress status of Fill bed. %1% is the name of the object being copied.
|
||||
ctl.update_status(st * 100 / status_range(), GUI::format(_u8L("Filling %1%"), str));
|
||||
};
|
||||
|
||||
params.on_packed = [&do_stop] (const ArrangePolygon &ap) {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
|
||||
|
||||
@@ -235,7 +236,8 @@ void OrientJob::process(Ctl &ctl)
|
||||
|
||||
params.progressind = [this, count, &ctl](unsigned st, std::string orientstr) {
|
||||
st += m_unprintable.size();
|
||||
if (st > 0) ctl.update_status(int(st / float(count) * 100), _u8L("Orienting") + " " + orientstr);
|
||||
// TRN Progress status. %1% is the name of the object being oriented.
|
||||
if (st > 0) ctl.update_status(int(st / float(count) * 100), GUI::format(_u8L("Orienting %1%"), orientstr));
|
||||
};
|
||||
|
||||
orientation::orient(m_selected, m_unselected, params);
|
||||
|
||||
@@ -104,7 +104,7 @@ class PlaterWorker: public Worker {
|
||||
if (eptr) try {
|
||||
std::rethrow_exception(eptr);
|
||||
} catch (std::exception &e) {
|
||||
show_error(m_plater, _L("An unexpected error occurred") + ": " + e.what());
|
||||
show_error(m_plater, _L("An unexpected error occurred") + ": " + from_u8(e.what()));
|
||||
eptr = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ class SLAImportDialog : public wxDialog, public SLAImportJobView
|
||||
wxComboBox *m_import_dropdown, *m_quality_dropdown;
|
||||
|
||||
public:
|
||||
SLAImportDialog(Plater *plater) : wxDialog{plater, wxID_ANY, "Import SLA archive"}
|
||||
SLAImportDialog(Plater *plater) : wxDialog{plater, wxID_ANY, _L("Import SLA archive")}
|
||||
{
|
||||
auto szvert = new wxBoxSizer{wxVERTICAL};
|
||||
auto szfilepck = new wxBoxSizer{wxHORIZONTAL};
|
||||
|
||||
m_filepicker = new wxFilePickerCtrl(this, wxID_ANY, from_u8(wxGetApp().app_config->get_last_dir()), _(L("Choose SLA archive:")),
|
||||
"SL1 / SL1S archive files (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP",
|
||||
_L("SL1 / SL1S archive files") + " (*.sl1, *.sl1s, *.zip)|*.sl1;*.SL1;*.sl1s;*.SL1S;*.zip;*.ZIP",
|
||||
wxDefaultPosition, wxDefaultSize, wxFLP_DEFAULT_STYLE | wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
|
||||
szfilepck->Add(new wxStaticText(this, wxID_ANY, _L("Import file") + ": "), 0, wxALIGN_CENTER);
|
||||
|
||||
@@ -76,7 +76,7 @@ void SLAImportJob::process(Ctl &ctl)
|
||||
break;
|
||||
}
|
||||
} catch (MissingProfileError &) {
|
||||
p->err = _L("The SLA archive doesn\'t contain any presets. Please activate some SLA printer presets first before importing that SLA archive.").ToStdString();
|
||||
p->err = _u8L("The SLA archive doesn\'t contain any presets. Please activate some SLA printer presets first before importing that SLA archive.");
|
||||
} catch (std::exception &ex) {
|
||||
p->err = ex.what();
|
||||
}
|
||||
@@ -124,8 +124,8 @@ void SLAImportJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
p->plater->get_notification_manager()->push_notification(
|
||||
NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::WarningNotificationLevel,
|
||||
_L("The imported SLA archive did not contain any presets. "
|
||||
"The current SLA presets were used as fallback.").ToStdString());
|
||||
_u8L("The imported SLA archive did not contain any presets. "
|
||||
"The current SLA presets were used as fallback."));
|
||||
}
|
||||
|
||||
if (p->sel != Sel::modelOnly) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
|
||||
@@ -26,6 +27,7 @@
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <array>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
@@ -74,7 +76,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color, m_input.volume_to_world, nullptr, &m_stats));
|
||||
color, m_input.volume_to_world, nullptr, &m_stats,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
|
||||
|
||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||
@@ -126,16 +129,71 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
{ mix.a_percent(), 100 - mix.a_percent() });
|
||||
});
|
||||
|
||||
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
|
||||
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
|
||||
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
|
||||
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
|
||||
// the triangles matter - the annotations are per facet and reference no vertex index - so a
|
||||
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
|
||||
// reason to remap anything.
|
||||
const bool topology_changed = m_input.options.pipeline_v2 ||
|
||||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
|
||||
|
||||
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
|
||||
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
|
||||
// no longer existed, which is what scrambled the colours of an imported coloured model even
|
||||
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
|
||||
//
|
||||
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
|
||||
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
|
||||
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
|
||||
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
|
||||
// could only lose detail.
|
||||
std::array<TriangleSelector::TriangleSplittingData, 4> before;
|
||||
if (topology_changed) {
|
||||
before[0] = volume->supported_facets.get_data();
|
||||
before[1] = volume->seam_facets.get_data();
|
||||
before[2] = volume->mmu_segmentation_facets.get_data();
|
||||
before[3] = volume->fuzzy_skin_facets.get_data();
|
||||
}
|
||||
|
||||
volume->set_mesh(std::move(m_result));
|
||||
volume->set_new_unique_id();
|
||||
volume->calculate_convex_hull();
|
||||
|
||||
if (topology_changed) {
|
||||
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
|
||||
&volume->mmu_segmentation_facets,
|
||||
&volume->fuzzy_skin_facets };
|
||||
for (size_t ch = 0; ch < target.size(); ++ch) {
|
||||
const std::vector<uint8_t> state =
|
||||
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
|
||||
// Nothing was painted in this channel, or there is nothing to paint it onto: either
|
||||
// way the annotation must not keep describing the mesh that is gone.
|
||||
if (state.empty()) {
|
||||
target[ch]->reset();
|
||||
continue;
|
||||
}
|
||||
TriangleSelector selector(volume->mesh());
|
||||
bool any = false;
|
||||
for (size_t i = 0; i < state.size(); ++i)
|
||||
if (state[i] != 0) {
|
||||
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
|
||||
any = true;
|
||||
}
|
||||
if (any)
|
||||
target[ch]->set(selector);
|
||||
else
|
||||
target[ch]->reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
|
||||
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
|
||||
// and one the user never painted at all stays at NONE, which already means "the volume's own
|
||||
// filament". That is what confines the effect to the painted area without having to invent a
|
||||
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
|
||||
// topology-preserving, so triangle i is still triangle i.
|
||||
// colour for everything outside it. What was already painted is whatever the transfer above
|
||||
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
|
||||
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
|
||||
TriangleSelector selector(volume->mesh());
|
||||
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
|
||||
@@ -159,7 +217,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
|
||||
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
|
||||
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
|
||||
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
|
||||
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
|
||||
@@ -167,6 +224,14 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
|
||||
volume->texture_displacement_facet(slot).reset();
|
||||
}
|
||||
|
||||
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
|
||||
// from: a displaced vertex came from its foot point on that surface, which is the position its
|
||||
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
|
||||
if (!m_input.model_uv.empty())
|
||||
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
|
||||
volume->mesh().its) :
|
||||
m_input.model_uv;
|
||||
|
||||
ModelObject *object = volume->get_object();
|
||||
if (object == nullptr)
|
||||
return;
|
||||
@@ -219,6 +284,8 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
||||
input.take_snapshot = take_snapshot;
|
||||
input.volume_id = volume.id();
|
||||
input.base_mesh = volume.mesh().its;
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
input.options = volume.texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
|
||||
@@ -34,6 +34,9 @@ struct TextureDisplacementBakeInput
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
|
||||
// layer maps through, and what the commit puts back on the baked mesh.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// Whether this job pushes its own undo step when it commits. False when the caller has already
|
||||
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
|
||||
// and subdivides first and has to undo as a single action.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "slic3r/GUI/Jobs/Worker.hpp"
|
||||
#include <memory>
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
@@ -52,13 +53,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
|
||||
indexed_triangle_set mesh = m_input.base_mesh;
|
||||
TextureDisplacementFacetsData masks = m_input.facets_data;
|
||||
std::vector<Vec2f> uv = m_input.model_uv;
|
||||
|
||||
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
|
||||
// preparation - which is where "first it remeshes, then it refines" actually happens. The
|
||||
// experimental pipeline refines as part of the bake and has nothing to prepare.
|
||||
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
|
||||
const TextureDisplacementPrepareResult prepared =
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
|
||||
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
|
||||
m_input.color,
|
||||
// Preparation is roughly half the run; the bake
|
||||
// takes the progress bar from there.
|
||||
@@ -77,12 +79,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
|
||||
if (!prepared.mesh.indices.empty()) {
|
||||
mesh = prepared.mesh;
|
||||
masks = prepared.masks;
|
||||
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
|
||||
}
|
||||
}
|
||||
|
||||
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
|
||||
[&report](int pct) { return report(50 + pct / 2); },
|
||||
/* color */ nullptr, m_input.volume_to_world, &recorder);
|
||||
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
|
||||
uv.empty() ? nullptr : &uv);
|
||||
|
||||
m_stages = recorder.take();
|
||||
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
|
||||
@@ -115,6 +119,8 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
|
||||
input.check_topology = check_topology;
|
||||
input.color = color;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
if (volume.has_texture_uv())
|
||||
input.model_uv = volume.texture_uv;
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ struct TextureDisplacementDebugInput
|
||||
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
|
||||
// produces no colour of its own.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
|
||||
// ModelUV layer actually bakes through. Empty when the volume has none.
|
||||
std::vector<Vec2f> model_uv;
|
||||
|
||||
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
|
||||
// multi-million triangle stage costs more than the stage did. On by default because a stage that
|
||||
|
||||
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
|
||||
// idle loop.
|
||||
int last_reported = 1;
|
||||
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
|
||||
m_input.params, m_input.color,
|
||||
m_input.model_uv, m_input.params, m_input.color,
|
||||
[&ctl, &status, &last_reported](int percent) {
|
||||
if (ctl.was_canceled())
|
||||
return false;
|
||||
@@ -106,6 +106,10 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
|
||||
volume->restore_painting(saved_painting);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
|
||||
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
|
||||
// restore_painting(), which resets it along with the extra facets for the same reason: it is
|
||||
// indexed by triangle and the triangles have changed.
|
||||
volume->texture_uv = std::move(m_result.uv);
|
||||
|
||||
if (ObjectList *obj_list = wxGetApp().obj_list()) {
|
||||
const ModelObjectPtrs &objs = plater->model().objects;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Job.hpp"
|
||||
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -47,6 +48,10 @@ struct TextureDisplacementPrepareInput
|
||||
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
|
||||
// perceived colour, never the interleaving that realises a mix.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
|
||||
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
|
||||
// indexed by triangle and so cannot survive the stages on its own.
|
||||
std::vector<Vec2f> model_uv;
|
||||
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
|
||||
// displacement job that follows commits into this same step rather than pushing its own.
|
||||
std::string snapshot_name;
|
||||
|
||||
@@ -48,7 +48,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
},
|
||||
color, m_input.volume_to_world);
|
||||
color, m_input.volume_to_world, nullptr, nullptr,
|
||||
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
|
||||
@@ -29,6 +29,9 @@ struct TextureDisplacementPreviewInput
|
||||
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
|
||||
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
|
||||
TextureColorSettings color;
|
||||
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
|
||||
// same layout it will bake through. Empty when the volume has none.
|
||||
std::vector<Vec2f> model_uv;
|
||||
};
|
||||
|
||||
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
|
||||
|
||||
@@ -200,6 +200,22 @@ Slic3r::PrintEstimatedStatistics::ETimeMode convert(const ETimeMode& mode)
|
||||
}
|
||||
}
|
||||
|
||||
// libvgcode finds where a path starts from a phantom vertex placed at the previous move's
|
||||
// position. Every move that is not an option marker (extrusions, travels, wipes, and Noop or
|
||||
// unmapped moves) starts a path when it is the first move, or when it differs from the previous
|
||||
// move in type, role or a value the preview colors by. convert() sizes its vertex array by
|
||||
// counting these, so the count and the fill must both use this test.
|
||||
static bool starts_path(const Slic3r::GCodeProcessorResult::MoveVertex& prev,
|
||||
const Slic3r::GCodeProcessorResult::MoveVertex& curr, bool first_move)
|
||||
{
|
||||
const EOptionType option_type = move_type_to_option(convert(curr.type));
|
||||
if (option_type != EOptionType::COUNT && option_type != EOptionType::Travels && option_type != EOptionType::Wipes)
|
||||
return false;
|
||||
return first_move || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
|
||||
// ORCA: Split the path when a preview value changes.
|
||||
|| prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk;
|
||||
}
|
||||
|
||||
GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::vector<std::string>& str_tool_colors,
|
||||
const std::vector<std::string>& str_color_print_colors, const Viewer& viewer,
|
||||
const Slic3r::Transform3d* belt_xform)
|
||||
@@ -231,7 +247,14 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
|
||||
}
|
||||
|
||||
const std::vector<Slic3r::GCodeProcessorResult::MoveVertex>& moves = result.moves;
|
||||
ret.vertices.reserve(2 * moves.size());
|
||||
// One vertex per move after the first, plus a phantom in front of every path start. Reserving
|
||||
// the exact count leaves nothing to shrink afterwards, and shrinking would copy the whole array
|
||||
// while both copies are alive.
|
||||
size_t vertices_count = moves.empty() ? 0 : moves.size() - 1;
|
||||
for (size_t i = 1; i < moves.size(); ++i)
|
||||
if (starts_path(moves[i - 1], moves[i], i == 1))
|
||||
++vertices_count;
|
||||
ret.vertices.reserve(vertices_count);
|
||||
// libvgcode numbers its layers from the vertices it is given and expects them to
|
||||
// arrive one layer after the other with consecutive ids: a layer without any move
|
||||
// (a belt file can carry layer changes that print nothing) would leave a gap, and
|
||||
@@ -248,33 +271,28 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
|
||||
++ layer_id;
|
||||
src_layer_id = curr.layer_id;
|
||||
}
|
||||
const EOptionType option_type = move_type_to_option(curr_type);
|
||||
if (option_type == EOptionType::COUNT || option_type == EOptionType::Travels || option_type == EOptionType::Wipes) {
|
||||
if (ret.vertices.empty() || prev.type != curr.type || prev.extrusion_role != curr.extrusion_role
|
||||
// ORCA: Split the path when a preview value changes.
|
||||
|| prev.mm3_per_mm != curr.mm3_per_mm || prev.acceleration != curr.acceleration || prev.jerk != curr.jerk) {
|
||||
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
|
||||
// equal to the current one with the exception of the position, which should match the previous move position,
|
||||
// and the times, which are set to zero
|
||||
if (starts_path(prev, curr, i == 1)) {
|
||||
// to allow libvgcode to properly detect the start/end of a path we need to add a 'phantom' vertex
|
||||
// equal to the current one with the exception of the position, which should match the previous move position,
|
||||
// and the times, which are set to zero
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
|
||||
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
|
||||
static_cast<uint32_t>(curr.gcode_id), layer_id,
|
||||
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
|
||||
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
|
||||
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
|
||||
/* ORCA: Add Jerk visualization support */ curr.jerk };
|
||||
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
|
||||
curr.mm3_per_mm, curr.fan_speed, curr.temperature, 0.0f, convert(curr.extrusion_role), curr_type,
|
||||
static_cast<uint32_t>(curr.gcode_id), layer_id,
|
||||
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
|
||||
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
|
||||
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
|
||||
/* ORCA: Add Jerk visualization support */ curr.jerk };
|
||||
#else
|
||||
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
|
||||
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
|
||||
static_cast<uint32_t>(curr.gcode_id), layer_id,
|
||||
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
|
||||
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
|
||||
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
|
||||
/* ORCA: Add Jerk visualization support */ curr.jerk };
|
||||
const libvgcode::PathVertex vertex = { xform_pos(prev.position), curr.height, curr.width, curr.feedrate, prev.actual_feedrate,
|
||||
curr.mm3_per_mm, curr.fan_speed, curr.temperature, convert(curr.extrusion_role), curr_type,
|
||||
static_cast<uint32_t>(curr.gcode_id), layer_id,
|
||||
static_cast<uint8_t>(curr.extruder_id), static_cast<uint8_t>(curr.cp_color_id), { 0.0f, 0.0f },
|
||||
/* ORCA: Add Pressure Advance visualization support */ 0.0f, curr.pressure_advance,
|
||||
/* ORCA: Add Acceleration visualization support */ curr.acceleration,
|
||||
/* ORCA: Add Jerk visualization support */ curr.jerk };
|
||||
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
ret.vertices.emplace_back(vertex);
|
||||
}
|
||||
ret.vertices.emplace_back(vertex);
|
||||
}
|
||||
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
@@ -297,6 +315,8 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
|
||||
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
ret.vertices.emplace_back(vertex);
|
||||
}
|
||||
assert(ret.vertices.size() == vertices_count);
|
||||
// A no-op when the count above is exact; restores the exact fit if the count ever drifts.
|
||||
ret.vertices.shrink_to_fit();
|
||||
|
||||
// Note: the belt designed-view anchoring (recovering the per-object placement/
|
||||
|
||||
@@ -1746,7 +1746,8 @@ bool MainFrame::preview_only_hint()
|
||||
confirm_dlg.update_btn_label(_L("Yes"), _L("No"));
|
||||
auto filename = m_plater->get_preview_only_filename();
|
||||
|
||||
confirm_dlg.update_text(filename + " " + _L("will be closed before creating a new model. Do you want to continue?"));
|
||||
// TRN %s is a file name.
|
||||
confirm_dlg.update_text(wxString::Format(_L("%s will be closed before creating a new model. Do you want to continue?"), from_u8(filename)));
|
||||
confirm_dlg.on_show();
|
||||
if (preview_only_to_editor) {
|
||||
m_plater->new_project();
|
||||
@@ -2827,6 +2828,10 @@ wxMenu* MainFrame::generate_help_menu()
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Show Configuration Folder"), _L("Show Configuration Folder"),
|
||||
[](wxCommandEvent&) { Slic3r::GUI::desktop_open_datadir_folder(); });
|
||||
|
||||
append_menu_item(helpMenu, wxID_ANY, _L("Migrate Filament Presets"),
|
||||
_L("Bring back filament presets that a profile update removed from printers they were made for"),
|
||||
[](wxCommandEvent&) { wxGetApp().check_legacy_user_filament_presets(true); });
|
||||
|
||||
helpMenu->AppendSeparator();
|
||||
|
||||
// Troubleshoot center
|
||||
|
||||
@@ -144,7 +144,7 @@ wxString DeviceItem::get_state_device()
|
||||
str_state_device.push_back(_L("Printing Failed"));
|
||||
str_state_device.push_back(_L("Printing"));
|
||||
str_state_device.push_back(_L("Printing Pause"));
|
||||
str_state_device.push_back(_L("Prepare"));
|
||||
str_state_device.push_back(_L_CONTEXT("Prepare", "Device status"));
|
||||
str_state_device.push_back(_L("Slicing"));
|
||||
str_state_device.push_back(_L("Syncing"));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "MultiMachineManagerPage.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -253,7 +254,7 @@ void MultiMachineItem::doRender(wxDC& dc)
|
||||
dc.DrawRoundedRectangle(left, (size.y - FromDIP(38)) / 2, FromDIP(90), FromDIP(38), 6);
|
||||
dc.SetFont(Label::Body_14);
|
||||
dc.SetTextForeground(*wxBLACK);
|
||||
dc.DrawText(_L("View"),left + FromDIP(90) / 2 - dc.GetTextExtent(_L("View")).x / 2, (size.y -dc.GetTextExtent(_L("View")).y) / 2);
|
||||
dc.DrawText(_L_CONTEXT("View", "Verb"),left + FromDIP(90) / 2 - dc.GetTextExtent(_L_CONTEXT("View", "Verb")).x / 2, (size.y -dc.GetTextExtent(_L_CONTEXT("View", "Verb")).y) / 2);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ MultiTaskItem::MultiTaskItem(wxWindow* parent, MachineObject* obj, int type)
|
||||
m_button_cancel->SetCornerRadius(6);
|
||||
m_button_cancel->SetMinSize(wxSize(FromDIP(70), FromDIP(35)));
|
||||
|
||||
m_button_pause = new Button(this, _L("Pause"));
|
||||
m_button_pause = new Button(this, _L_CONTEXT("Pause", "Verb"));
|
||||
m_button_pause->SetBackgroundColor(clean_bg);
|
||||
m_button_pause->SetBorderColor(clean_bd);
|
||||
m_button_pause->SetTextColor(clean_text);
|
||||
@@ -1193,7 +1193,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent)
|
||||
m_ctrl_btn_panel->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), -1));
|
||||
m_ctrl_btn_panel->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), -1));
|
||||
m_btn_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
btn_pause_all = new Button(m_ctrl_btn_panel, _L("Pause"));
|
||||
btn_pause_all = new Button(m_ctrl_btn_panel, _L_CONTEXT("Pause", "Verb"));
|
||||
btn_pause_all->SetBackgroundColor(ctrl_bg);
|
||||
btn_pause_all->SetCornerRadius(FromDIP(5));
|
||||
btn_continue_all = new Button(m_ctrl_btn_panel, _L("Resume"));
|
||||
|
||||
@@ -365,7 +365,7 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
|
||||
});
|
||||
add_with_icon("obj_drop", L("Drop to Bed"), L("Object"), "toolbar_flatten", [](const std::string&) {
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
|
||||
return object_op(wxGetApp().plater(), [](Plater* p) { return p->get_selection().can_drop(); }, [](Plater* p) { p->drop_selection(); });
|
||||
});
|
||||
add("obj_fit_volume", L("Scale to Fit Print Volume"), L("Object"), [](const std::string&) {
|
||||
return object_op(
|
||||
@@ -589,6 +589,10 @@ std::vector<NativeCommand> build_command_catalog()
|
||||
Slic3r::GUI::desktop_open_datadir_folder();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_migrate_filament_presets", L("Migrate Filament Presets"), L("Help"), [](const std::string&) {
|
||||
wxGetApp().check_legacy_user_filament_presets(true);
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
});
|
||||
add("help_troubleshoot", L("Troubleshoot Center"), L("Help"), [](const std::string&) {
|
||||
wxGetApp().troubleshoot();
|
||||
return AppActionRunResult{AppActionRunResult::Level::Success};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/GCodeStatistics.hpp"
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
@@ -2147,13 +2148,6 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
std::string cur = get_imex_mode();
|
||||
const std::string effective = get_effective_imex_mode();
|
||||
if (cur == kImexPrimaryMode) cur = _u8L("Primary");
|
||||
if (effective != get_imex_mode()) {
|
||||
try {
|
||||
cur += (boost::format(_u8L(" (process preset: %1%)")) % effective).str();
|
||||
} catch (const std::exception&) {
|
||||
cur += (boost::format(" (process preset: %1%)") % effective).str();
|
||||
}
|
||||
}
|
||||
// One format string, not two catalog fragments concatenated around a
|
||||
// runtime value: translators need to move the mode name within the
|
||||
// sentence, and the space-padded fragments were untranslatable alone.
|
||||
@@ -2162,10 +2156,19 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
|
||||
// TRANSLATED string drops or malforms %1%, and an exception escaping here
|
||||
// would take down the frame rather than show a wrong tooltip. The English
|
||||
// literal is the fallback and cannot itself throw.
|
||||
try {
|
||||
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)")) % cur).str();
|
||||
} catch (const std::exception&) {
|
||||
hover_tip = (boost::format("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)") % cur).str();
|
||||
if (effective != get_imex_mode()) {
|
||||
try {
|
||||
// TRN Plate icon tooltip. %1% is the plate's IDEX/IQEX mode, %2% the mode its process preset sets.
|
||||
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (process preset: %2%) (left-click to cycle, right-click for menu)")) % cur % effective).str();
|
||||
} catch (const std::exception&) {
|
||||
hover_tip = (boost::format("IDEX/IQEX mode: %1% (process preset: %2%) (left-click to cycle, right-click for menu)") % cur % effective).str();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
hover_tip = (boost::format(_u8L("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)")) % cur).str();
|
||||
} catch (const std::exception&) {
|
||||
hover_tip = (boost::format("IDEX/IQEX mode: %1% (left-click to cycle, right-click for menu)") % cur).str();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
render_icon_texture(m_imex_mode_icon.model, m_partplate_list->m_imex_mode_textures[kind]);
|
||||
@@ -3034,8 +3037,8 @@ bool PartPlate::check_filament_printable(const DynamicPrintConfig &config, wxStr
|
||||
int filament_printable_status = filament_printables[filament_id];
|
||||
int extruder_idx = filament_map[filament_id] - 1;
|
||||
if (!(filament_printable_status >> extruder_idx & 1)) {
|
||||
wxString extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
|
||||
error_message = wxString::Format(_L("The %s nozzle can not print %s."), extruder_name, filament_type);
|
||||
error_message = wxString::Format(extruder_idx == 0 ? _L("The left nozzle cannot print %s.") : _L("The right nozzle cannot print %s."),
|
||||
filament_type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3228,9 +3231,8 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
|
||||
auto get_nozzle_msg = [](const float nozzle_diameter, const NozzleVolumeType volume_type) -> std::string {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(1) << nozzle_diameter;
|
||||
std::string nozzle_msg = oss.str();
|
||||
((nozzle_msg += "mm ") += _u8L(get_nozzle_volume_type_string(volume_type))) += _u8L(" nozzle");
|
||||
return nozzle_msg;
|
||||
// TRN %1% is the nozzle diameter, %2% the nozzle volume type, for example "Standard" or "High Flow".
|
||||
return Slic3r::format(_u8L("%1%mm %2% nozzle"), oss.str(), _u8L(get_nozzle_volume_type_string(volume_type)));
|
||||
};
|
||||
|
||||
auto get_incompatible_filament_msg = [](const std::set<std::string> &incompatible_selected_filaments) -> std::string {
|
||||
@@ -3261,7 +3263,9 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
|
||||
auto elem = incompatible_selected_map.begin();
|
||||
NozzleVolumeType volume_type = elem->first;
|
||||
auto incompatible_selected = elem->second;
|
||||
error_msg = GUI::format(_L("It is not recommended to print the following filament(s) with %1%: %2%\n"), get_nozzle_msg(nozzle_diameter, volume_type),
|
||||
error_msg = GUI::format(_L_PLURAL("It is not recommended to print the following filament with %1%: %2%\n",
|
||||
"It is not recommended to print the following filaments with %1%: %2%\n", incompatible_selected.size()),
|
||||
get_nozzle_msg(nozzle_diameter, volume_type),
|
||||
get_incompatible_filament_msg(incompatible_selected));
|
||||
} else {
|
||||
std::string warning_msg = _u8L("It is not recommended to use the following nozzle and filament combinations:\n");
|
||||
@@ -4669,8 +4673,8 @@ void PartPlate::update_slice_context(BackgroundSlicingProcess & process)
|
||||
Slic3r::SlicingStatusEvent *event = new Slic3r::SlicingStatusEvent(EVT_SLICING_UPDATE, 0, status);
|
||||
//BBS: GUI refactor: add plate info befor message
|
||||
if (status.message_type == Slic3r::PrintStateBase::SlicingDefaultNotification) {
|
||||
auto temp = Slic3r::format(_u8L(" plate %1%:"), std::to_string(m_plate_index + 1));
|
||||
event->status.text = temp + event->status.text;
|
||||
// TRN Slicing progress. %1% is the plate number, %2% the current slicing step.
|
||||
event->status.text = Slic3r::format(_u8L("Slicing plate %1%: %2%"), m_plate_index + 1, event->status.text);
|
||||
}
|
||||
wxQueueEvent(m_plater, event);
|
||||
};
|
||||
@@ -6151,6 +6155,14 @@ int PartPlateList::select_plate(int index)
|
||||
//wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT));
|
||||
}
|
||||
|
||||
// With the other plates hidden, the view follows the selected plate.
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && m_plater->is_view3D_shown() && wxGetApp().focus_mode()) {
|
||||
Camera& camera = m_plater->get_camera();
|
||||
Vec3d target = camera.get_target();
|
||||
target.head<2>() = m_plate_list[index]->get_bounding_box().center().head<2>();
|
||||
camera.set_target(target);
|
||||
}
|
||||
|
||||
if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) {
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = m_plate_list[index]->get_plate_name();
|
||||
@@ -7060,7 +7072,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
|
||||
/*rendering related functions*/
|
||||
//render
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome, bool hide_inactive)
|
||||
{
|
||||
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
|
||||
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
|
||||
@@ -7075,7 +7087,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
load_icon_textures();
|
||||
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
|
||||
int current_index = (*it)->get_index();
|
||||
if (only_current && (current_index != m_current_plate))
|
||||
if ((only_current || hide_inactive) && (current_index != m_current_plate))
|
||||
continue;
|
||||
if (current_index == m_current_plate) {
|
||||
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
|
||||
@@ -7536,7 +7548,7 @@ int PartPlateList::rebuild_plates_after_arrangement(bool recycle_plates, bool ex
|
||||
return ret;
|
||||
}
|
||||
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx)
|
||||
int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info, int plate_idx, bool with_statistics)
|
||||
{
|
||||
int ret = 0;
|
||||
|
||||
@@ -7610,6 +7622,8 @@ 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());
|
||||
if (with_statistics)
|
||||
plate_data_item->statistics = gcode_statistics_json(*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
|
||||
|
||||
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
/*rendering related functions*/
|
||||
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false, bool hide_inactive = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void render_hover_tooltip() const;
|
||||
@@ -1070,7 +1070,7 @@ public:
|
||||
/* load/store releted functions, with_gcode = true and plate_idx = -1, export all gcode
|
||||
* if with_gcode = true and specify plate_idx, export plate_idx gcode only
|
||||
*/
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1);
|
||||
int store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool with_slice_info = true, int plate_idx = -1, bool with_statistics = false);
|
||||
int load_from_3mf_structure(PlateDataPtrs& plate_data_list, int filament_count = 1);
|
||||
//load gcode files
|
||||
int load_gcode_files();
|
||||
|
||||
@@ -676,6 +676,7 @@ void PartSkipDialog::UpdateCountLabel()
|
||||
}
|
||||
m_cnt_label->SetLabel(wxString::Format("%d", check_cnt));
|
||||
m_cnt_label->Fit();
|
||||
// TRN Shown right after the number of selected objects, which is a separate label: "3/10 Selected". %d is the total number of objects.
|
||||
m_tot_label->SetLabel(wxString::Format(_L("/%d Selected"), tot_cnt));
|
||||
m_tot_label->Fit();
|
||||
m_dlg_btn_sizer->Layout();
|
||||
@@ -922,7 +923,8 @@ void PartSkipDialog::OnApplyDialog(wxCommandEvent &event)
|
||||
confirm_dialog.SetMsgLabel(_L("Skipping all objects."));
|
||||
confirm_dialog.SetTipLabel(_L("The printing job will be stopped. Continue?"));
|
||||
} else {
|
||||
confirm_dialog.SetMsgLabel(wxString::Format(_L("Skipping %d objects."), m_partskip_ids.size()));
|
||||
const int skip_cnt = int(m_partskip_ids.size());
|
||||
confirm_dialog.SetMsgLabel(wxString::Format(_L_PLURAL("Skipping %d object.", "Skipping %d objects.", skip_cnt), skip_cnt));
|
||||
confirm_dialog.SetTipLabel(_L("This action cannot be undone. Continue?"));
|
||||
}
|
||||
|
||||
|
||||
+185
-100
@@ -436,6 +436,7 @@ void Plater::show_illegal_characters_warning(wxWindow* parent)
|
||||
void Plater::mark_plate_toolbar_image_dirty()
|
||||
{
|
||||
m_b_plate_toolbar_image_dirty = true;
|
||||
++m_plate_toolbar_image_version;
|
||||
}
|
||||
|
||||
bool Plater::is_plate_toolbar_image_dirty() const
|
||||
@@ -1686,7 +1687,7 @@ ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row()
|
||||
plater->update_machine_sync_status();
|
||||
}
|
||||
});
|
||||
combo_flow->SetToolTip(_L("Flow"));
|
||||
combo_flow->SetToolTip(_L_CONTEXT("Flow", "Nozzle Flow"));
|
||||
row.flow = combo_flow;
|
||||
use_nozzle_row_font(combo_flow);
|
||||
|
||||
@@ -4125,13 +4126,12 @@ void Sidebar::update_presets(Preset::Type preset_type)
|
||||
std::vector<NozzleType> nozzle_types(cfg_nozzle_type->size());
|
||||
for (size_t idx = 0; idx < cfg_nozzle_type->size(); ++idx)
|
||||
nozzle_types[idx] = NozzleType(cfg_nozzle_type->values[idx]);
|
||||
nozzle_type = _L( // NEEDFIX this part can be replaced with shorter names
|
||||
nozzle_types[0] == ntHardenedSteel ? "Hardened Steel" :
|
||||
nozzle_types[0] == ntStainlessSteel ? "Stainless Steel" :
|
||||
nozzle_types[0] == ntTungstenCarbide ? "Tungsten Carbide" :
|
||||
nozzle_types[0] == ntBrass ? "Brass"
|
||||
: "-" // Undefined
|
||||
);
|
||||
// NEEDFIX this part can be replaced with shorter names
|
||||
nozzle_type = nozzle_types[0] == ntHardenedSteel ? _L("Hardened Steel") :
|
||||
nozzle_types[0] == ntStainlessSteel ? _L("Stainless Steel") :
|
||||
nozzle_types[0] == ntTungstenCarbide ? _L("Tungsten Carbide") :
|
||||
nozzle_types[0] == ntBrass ? _L("Brass")
|
||||
: wxString("-"); // Undefined
|
||||
}
|
||||
p->label_nozzle_type->SetLabel(nozzle_type);
|
||||
p->label_nozzle_type->SetToolTip(nozzle_type == "-" ? "" : nozzle_type);
|
||||
@@ -4965,7 +4965,7 @@ void Sidebar::collect_physical_filament_info(std::vector<std::string>& color_str
|
||||
|
||||
for (size_t i = 0; i < num_physical; ++i) {
|
||||
auto* combo = p->combos_filament[i];
|
||||
names.push_back(combo ? into_u8(combo->GetValue()) : "Filament " + std::to_string(i + 1));
|
||||
names.push_back(into_u8(combo ? combo->GetValue() : wxString::Format(_L("Filament %d"), int(i + 1))));
|
||||
}
|
||||
|
||||
auto& preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -5333,7 +5333,7 @@ void Sidebar::remove_all_mixed_filaments()
|
||||
|
||||
// Worth a confirmation: this drops filament slots the model may be painted with, and anything
|
||||
// painted in one falls back to a plain filament.
|
||||
MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"),
|
||||
MessageDialog dlg(this, format_wxstr(_L_PLURAL("Remove %1% mixed filament?", "Remove all %1% mixed filaments?", count), count), _L("Mixed Filament"),
|
||||
wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
|
||||
if (dlg.ShowModal() != wxID_YES)
|
||||
return;
|
||||
@@ -6014,7 +6014,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na
|
||||
|
||||
bool Sidebar::is_new_project_in_gcode3mf()
|
||||
{
|
||||
if (p->plater->using_exported_file()) {
|
||||
// A .gcode previewed with its own settings shows them here too, and is closed the same way.
|
||||
if (p->plater->using_exported_file() || p->plater->gcode_settings_loaded()) {
|
||||
auto filename = p->plater->get_preview_only_filename();
|
||||
auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename);
|
||||
MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"),
|
||||
@@ -7368,6 +7369,9 @@ struct Plater::priv
|
||||
void enable_sidebar(bool enabled);
|
||||
void collapse_sidebar(bool collapse);
|
||||
void update_sidebar(bool force_update = false);
|
||||
// The sidebar shows the selected presets, which a G-code previewed without its own settings has nothing to do
|
||||
// with, so it stays hidden then. With the file's settings loaded it shows them, as for a .gcode.3mf.
|
||||
bool gcode_preview_without_settings() const { return q->m_only_gcode && !q->m_gcode_settings_loaded; }
|
||||
void reset_window_layout();
|
||||
Sidebar::DockingState get_sidebar_docking_state();
|
||||
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||
@@ -8593,7 +8597,7 @@ void Plater::priv::select_next_view_3D()
|
||||
|
||||
void Plater::priv::enable_sidebar(bool enabled)
|
||||
{
|
||||
if (q->m_only_gcode)
|
||||
if (gcode_preview_without_settings())
|
||||
enabled = false;
|
||||
|
||||
sidebar_layout.is_enabled = enabled;
|
||||
@@ -8602,7 +8606,7 @@ void Plater::priv::enable_sidebar(bool enabled)
|
||||
|
||||
void Plater::priv::collapse_sidebar(bool collapse)
|
||||
{
|
||||
if (q->m_only_gcode)
|
||||
if (gcode_preview_without_settings())
|
||||
return;
|
||||
|
||||
sidebar_layout.is_collapsed = collapse;
|
||||
@@ -8865,6 +8869,64 @@ static void log_substitutions(const PresetsConfigSubstitutions& substitutions, c
|
||||
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
|
||||
}
|
||||
|
||||
// After the settings of a project or a G-code were loaded into the presets: size the per-filament project
|
||||
// lists to the loaded filament count, and drop state only a connected printer may set.
|
||||
static void sync_loaded_project_filament_config(DynamicConfig& proj_cfg)
|
||||
{
|
||||
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
|
||||
if (filament_color) {
|
||||
size_t filament_count = filament_color->size();
|
||||
|
||||
// Sync filament map
|
||||
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
|
||||
if (filament_map->size() != filament_count) {
|
||||
filament_map->values.resize(filament_count, 1);
|
||||
}
|
||||
|
||||
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
|
||||
if (filament_nozzle_map->size() != filament_count) {
|
||||
filament_nozzle_map->values.resize(filament_count, 0);
|
||||
}
|
||||
|
||||
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
|
||||
if (filament_volume_map->size() != filament_count) {
|
||||
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
}
|
||||
|
||||
// Sync filament multi colour
|
||||
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
|
||||
if (filament_multi_color->size() != filament_count) {
|
||||
filament_multi_color->values.resize(filament_count);
|
||||
}
|
||||
// If there is no multi-color data or color is not match, use single color as default value
|
||||
for (size_t i = 0; i < filament_count; i++) {
|
||||
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
|
||||
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
|
||||
filament_multi_color->values[i] = filament_color->values[i];
|
||||
}
|
||||
}
|
||||
// Sync filament colour type
|
||||
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
|
||||
if (filament_color_type && filament_color_type->size() != filament_count) {
|
||||
filament_color_type->values.resize(filament_count);
|
||||
|
||||
for (size_t i = 0; i < filament_count; i++) {
|
||||
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
|
||||
filament_color_type->values[i] = "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament Track Switch state is derived live from the connected
|
||||
// printer; a loaded project must not carry a stale installed/active
|
||||
// flag, so clear both and let device sync re-derive them.
|
||||
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
|
||||
has_switcher->value = false;
|
||||
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
|
||||
dynamic_map->value = false;
|
||||
}
|
||||
|
||||
// BBS: backup & restore
|
||||
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
|
||||
LoadStrategy strategy,
|
||||
@@ -9681,58 +9743,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
if (file_wipe_tower_y)
|
||||
*wipe_tower_y = *file_wipe_tower_y;
|
||||
|
||||
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
|
||||
if (filament_color) {
|
||||
size_t filament_count = filament_color->size();
|
||||
|
||||
// Sync filament map
|
||||
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
|
||||
if (filament_map->size() != filament_count) {
|
||||
filament_map->values.resize(filament_count, 1);
|
||||
}
|
||||
|
||||
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
|
||||
if (filament_nozzle_map->size() != filament_count) {
|
||||
filament_nozzle_map->values.resize(filament_count, 0);
|
||||
}
|
||||
|
||||
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
|
||||
if (filament_volume_map->size() != filament_count) {
|
||||
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
|
||||
}
|
||||
|
||||
// Sync filament multi colour
|
||||
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
|
||||
if (filament_multi_color->size() != filament_count) {
|
||||
filament_multi_color->values.resize(filament_count);
|
||||
}
|
||||
// If there is no multi-color data or color is not match, use single color as default value
|
||||
for (size_t i = 0; i < filament_count; i++) {
|
||||
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
|
||||
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
|
||||
filament_multi_color->values[i] = filament_color->values[i];
|
||||
}
|
||||
}
|
||||
// Sync filament colour type
|
||||
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
|
||||
if (filament_color_type && filament_color_type->size() != filament_count) {
|
||||
filament_color_type->values.resize(filament_count);
|
||||
|
||||
for (size_t i = 0; i < filament_count; i++) {
|
||||
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
|
||||
filament_color_type->values[i] = "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filament Track Switch state is derived live from the connected
|
||||
// printer; a loaded project must not carry a stale installed/active
|
||||
// flag, so clear both and let device sync re-derive them.
|
||||
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
|
||||
has_switcher->value = false;
|
||||
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
|
||||
dynamic_map->value = false;
|
||||
sync_loaded_project_filament_config(proj_cfg);
|
||||
}
|
||||
// Update filament combobox after loading config
|
||||
if (published_config.published) {
|
||||
@@ -10360,10 +10371,7 @@ std::vector<size_t> Plater::priv::load_model_objects(const ModelObjectPtrs& mode
|
||||
}
|
||||
|
||||
if (!auto_drop) {
|
||||
for (size_t i = 0; i < object->instances.size(); ++i) {
|
||||
ModelInstance* instance = object->instances[i];
|
||||
instance->auto_drop = auto_drop;
|
||||
}
|
||||
object->auto_drop = false;
|
||||
|
||||
// if under the bed, move over the bed
|
||||
double dist_to_bed = std::min(object->min_z(), double(0));
|
||||
@@ -10992,7 +11000,7 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */)
|
||||
return false;
|
||||
};
|
||||
bool split_auto_drop = auto_drop;
|
||||
if (current_model_object->instances[0]->auto_drop && is_atleast_one_floating()) {
|
||||
if (current_model_object->auto_drop && is_atleast_one_floating()) {
|
||||
MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"),
|
||||
_L("Object with floating parts was detected"), wxICON_QUESTION | wxYES_NO);
|
||||
|
||||
@@ -12156,7 +12164,7 @@ void Plater::priv::reload_from_disk()
|
||||
}
|
||||
}
|
||||
else {
|
||||
wxString message = _L("Do you want to replace it") + " ?";
|
||||
wxString message = _L("Do you want to replace it?");
|
||||
MessageDialog dlg(q, message, _L("Message"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION);
|
||||
if (dlg.ShowModal() == wxID_YES)
|
||||
#if ENABLE_RELOAD_FROM_DISK_REWORK
|
||||
@@ -12564,7 +12572,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice)
|
||||
|
||||
// Add sidebar and toolbar collapse logic
|
||||
if (panel == view3D || panel == preview) {
|
||||
this->enable_sidebar(!q->only_gcode_mode());
|
||||
this->enable_sidebar(true);
|
||||
}
|
||||
if (panel == preview) {
|
||||
if (q->only_gcode_mode()) {
|
||||
@@ -13065,9 +13073,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
|
||||
void Plater::priv::on_slicing_update(SlicingStatusEvent &evt)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % evt.status.text;
|
||||
//BBS: add slice project logic
|
||||
std::string title_text = _u8L("Slicing");
|
||||
evt.status.text = title_text + evt.status.text;
|
||||
// The plate's status callback has already prefixed the text with "Slicing plate N:".
|
||||
if (evt.status.percent >= 0) {
|
||||
if (!m_worker.is_idle()) {
|
||||
// Avoid a race condition
|
||||
@@ -13635,8 +13641,8 @@ static std::vector<wxString> collect_imex_warnings(PartPlate* plate)
|
||||
const int secondary_bed_temp = bed_temps->values[secondary_logical];
|
||||
if (secondary_bed_temp > 0 && std::abs(secondary_bed_temp - primary_bed_temp) > 5) {
|
||||
warnings.push_back(wxString::Format(
|
||||
_L("Bed temperature conflict: T%d (%s) sets the bed to %d\u00B0C \u2014 "
|
||||
"T%d (%s) requires %d\u00B0C. The primary tool controls the bed; "
|
||||
_L("Bed temperature conflict: T%d (%s) sets the bed to %d℃ \u2014 "
|
||||
"T%d (%s) requires %d℃. The primary tool controls the bed; "
|
||||
"secondary tools print at whatever temperature it sets."),
|
||||
primary_tool, primary_display_type, primary_bed_temp,
|
||||
tool_idx, secondary_display_type, secondary_bed_temp));
|
||||
@@ -15698,7 +15704,7 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model,
|
||||
name = preset->label(false);
|
||||
}
|
||||
if (name.empty())
|
||||
name = "Filament " + std::to_string(i + 1);
|
||||
name = into_u8(wxString::Format(_L("Filament %d"), int(i + 1)));
|
||||
entry.name = name;
|
||||
|
||||
if (entry.kind == TextureFilamentKind::ExistingMixed) {
|
||||
@@ -16826,7 +16832,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
|
||||
if (accels.empty()) {
|
||||
accels.assign({accel});
|
||||
const auto msg{_L("INFO:") + "\n" +
|
||||
_L("No accelerations provided for calibration. Use default acceleration value ") + std::to_string(long(accel)) + _L(u8"mm/s²")};
|
||||
format_wxstr(_L("No accelerations provided for calibration. Use default acceleration value %1%mm/s²"), long(accel))};
|
||||
get_notification_manager()->push_notification(msg.utf8_string());
|
||||
} else {
|
||||
// set max acceleration in case of batch mode to get correct test pattern size
|
||||
@@ -16891,7 +16897,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
|
||||
|
||||
speeds.assign({speed});
|
||||
const auto msg{_L("INFO:") + "\n" +
|
||||
_L("No speeds provided for calibration. Use default optimal speed ") + std::to_string(long(speed)) + _L("mm/s")};
|
||||
format_wxstr(_L("No speeds provided for calibration. Use default optimal speed %1%mm/s"), long(speed))};
|
||||
get_notification_manager()->push_notification(msg.utf8_string());
|
||||
} else if (speeds.size() == 1) {
|
||||
// If we have single value provided, set speed using global configuration.
|
||||
@@ -17315,7 +17321,7 @@ void Plater::calib_temp(const Calib_Params& params) {
|
||||
if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") &&
|
||||
! boost::filesystem::exists(belt_temp_tower_asset(params))) {
|
||||
MessageDialog dlg(static_cast<wxWindow *>(wxGetApp().mainframe),
|
||||
format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. "
|
||||
format_wxstr(_L("No belt temperature tower is available for the range %1% to %2%℃. "
|
||||
"Use a range the tower models cover, for example 230 to 190."),
|
||||
(int) lround(params.start), (int) lround(params.end)),
|
||||
_L("Temperature tower"), wxICON_ERROR | wxOK);
|
||||
@@ -18073,11 +18079,80 @@ void Plater::load_gcode(const wxString& filename)
|
||||
|
||||
m_last_loaded_gcode = filename;
|
||||
|
||||
// An OrcaSlicer G-code carries the settings it was sliced with. They replace the selected presets as a
|
||||
// .gcode.3mf's do, so unsaved preset changes get the save-or-discard choice of opening a project rather
|
||||
// than the new project's choice to keep them.
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
const std::string path = into_u8(filename);
|
||||
std::optional<DynamicPrintConfig> file_config;
|
||||
ConfigSubstitutions substitutions;
|
||||
{
|
||||
wxBusyCursor wait;
|
||||
try {
|
||||
file_config = preset_bundle->config_from_gcode(path, ForwardCompatibilitySubstitutionRule::Enable, substitutions);
|
||||
} catch (const std::exception& ex) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no settings to load from " << path << ": " << ex.what();
|
||||
}
|
||||
}
|
||||
|
||||
// BSS: create a new project when load_gcode, force close previous one
|
||||
if (new_project(false, true) != wxID_YES)
|
||||
if (file_config) {
|
||||
auto check = [](bool yes_or_no) {
|
||||
return yes_or_no || wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified."));
|
||||
};
|
||||
if (close_with_confirm(check) == wxID_CANCEL || new_project(true, true) != wxID_YES)
|
||||
return;
|
||||
} else if (new_project(false, true) != wxID_YES)
|
||||
return;
|
||||
|
||||
m_only_gcode = true;
|
||||
m_gcode_settings_loaded = false;
|
||||
// A previewed G-code's settings are not the selection the app should remember.
|
||||
PresetBundle::SelectionExportBlocker no_selection_export(*preset_bundle);
|
||||
// What loading the file's settings replaces, to put back if that load fails or the file is rejected.
|
||||
std::optional<PresetBundle::Selection> previous;
|
||||
if (file_config) {
|
||||
previous = preset_bundle->selection();
|
||||
if (!previous)
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": project presets are still loaded, not loading the settings of " << path;
|
||||
}
|
||||
auto unload_file_settings = [this, preset_bundle, &previous]() {
|
||||
if (!previous)
|
||||
return;
|
||||
preset_bundle->restore_selection(*previous);
|
||||
wxGetApp().load_current_presets(false, false);
|
||||
// Switching back to the previous printer re-derives some project settings (TabPrinter::on_preset_loaded),
|
||||
// and only the printer refresh shows them in the sidebar's flow selectors.
|
||||
preset_bundle->project_config = std::move(previous->project_config);
|
||||
sidebar().update_presets(Preset::TYPE_PRINTER);
|
||||
reset_project_dirty_initial_presets();
|
||||
update_project_dirty_from_presets();
|
||||
m_gcode_settings_loaded = false;
|
||||
// The rejections after the switch to the preview have already shown the sidebar with the file's settings.
|
||||
enable_sidebar(false);
|
||||
previous.reset();
|
||||
};
|
||||
if (previous) {
|
||||
try {
|
||||
preset_bundle->load_config_model(path, std::move(*file_config));
|
||||
m_gcode_settings_loaded = true;
|
||||
} catch (const std::exception& ex) {
|
||||
unload_file_settings();
|
||||
get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::WarningNotificationLevel,
|
||||
GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), path) +
|
||||
"\n" + ex.what());
|
||||
}
|
||||
}
|
||||
if (m_gcode_settings_loaded) {
|
||||
wxGetApp().load_current_presets(false, false);
|
||||
sync_loaded_project_filament_config(preset_bundle->project_config);
|
||||
sidebar().update_presets(Preset::TYPE_FILAMENT);
|
||||
reset_project_dirty_initial_presets();
|
||||
update_project_dirty_from_presets();
|
||||
if (!substitutions.empty())
|
||||
show_substitutions_info(substitutions, path);
|
||||
}
|
||||
|
||||
// cleanup view before to start loading/processing
|
||||
//BBS: update gcode to current partplate's
|
||||
@@ -18105,6 +18180,7 @@ void Plater::load_gcode(const wxString& filename)
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
unload_file_settings();
|
||||
show_error(this, ex.what());
|
||||
return;
|
||||
}
|
||||
@@ -18152,7 +18228,8 @@ void Plater::load_gcode(const wxString& filename)
|
||||
p->preview->get_canvas3d()->zoom_to_plate(0);
|
||||
|
||||
if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) {
|
||||
MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("Does not contain valid G-code."),
|
||||
unload_file_settings();
|
||||
MessageDialog(this, format_wxstr(_L("The selected file:\n%1%\ndoes not contain valid G-code."), filename),
|
||||
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal();
|
||||
set_project_filename(_L(DEFAULT_PROJECT_NAME));
|
||||
} else {
|
||||
@@ -18192,16 +18269,20 @@ wxString Plater::get_project_name()
|
||||
|
||||
void Plater::update_all_plate_thumbnails(bool force_update)
|
||||
{
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++) {
|
||||
PartPlate* plate = get_partplate_list().get_plate(i);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
for (int i = 0; i < get_partplate_list().get_plate_count(); i++)
|
||||
update_plate_thumbnails(i, force_update);
|
||||
}
|
||||
|
||||
void Plater::update_plate_thumbnails(int plate_idx, bool force_update)
|
||||
{
|
||||
PartPlate* plate = get_partplate_list().get_plate(plate_idx);
|
||||
ThumbnailsParams thumbnail_params = { {}, false, true, true, true, plate_idx};
|
||||
if (force_update || !plate->thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho);
|
||||
}
|
||||
if (force_update || !plate->no_light_thumbnail_data.is_valid()) {
|
||||
get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params,
|
||||
Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19488,7 +19569,10 @@ void Plater::export_gcode_3mf(bool export_all)
|
||||
int plate_idx = get_partplate_list().get_curr_plate_index();
|
||||
if (export_all)
|
||||
plate_idx = PLATE_ALL_IDX;
|
||||
export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence
|
||||
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
|
||||
if (wxGetApp().app_config->get_bool("export_statistics"))
|
||||
strategy = strategy | SaveStrategy::WithStatistics;
|
||||
export_3mf(output_path, strategy, plate_idx);
|
||||
|
||||
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
|
||||
|
||||
@@ -20342,7 +20426,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
|
||||
|
||||
//BBS: add bbs 3mf logic
|
||||
PlateDataPtrs plate_data_list;
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx);
|
||||
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
|
||||
strategy & SaveStrategy::WithStatistics);
|
||||
|
||||
// BBS: backup
|
||||
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
@@ -23880,7 +23965,7 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
|
||||
|
||||
const PartPlate* plate = p->partplate_list.get_curr_plate();
|
||||
if (!plate) {
|
||||
t.label = "T" + std::to_string(physical_head) + " -> (no plate)";
|
||||
t.label = Slic3r::format(_u8L("T%1% -> (no plate)"), physical_head);
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -23891,8 +23976,8 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
|
||||
// No filament slot resolves to this physical head. Usually means the printer's
|
||||
// extruder count is understated relative to the filament palette (user added a
|
||||
// filament without extending printer_extruder_id in the Machine tab).
|
||||
t.label = "T" + std::to_string(physical_head) + " — no filament routed\n"
|
||||
"Add an extruder in the Machine tab so this head has a filament slot.";
|
||||
// TRN IDEX/IQEX ghost tooltip. %1% is the number of the physical print head.
|
||||
t.label = Slic3r::format(_u8L("T%1% — no filament routed\nAdd an extruder in the Machine tab so this head has a filament slot."), physical_head);
|
||||
return t;
|
||||
}
|
||||
t.filament_slot_1based = logical + 1;
|
||||
@@ -23900,8 +23985,8 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
|
||||
// from one resolution, not two.
|
||||
t.swatch = plate->get_imex_head_filament_color(physical_head, pem, map);
|
||||
t.swatch.a(1.0f); // tooltip swatch opaque
|
||||
t.label = "T" + std::to_string(physical_head) +
|
||||
" -> filament " + std::to_string(t.filament_slot_1based);
|
||||
// TRN IDEX/IQEX ghost tooltip. %1% is the number of the physical print head, %2% the filament it prints.
|
||||
t.label = Slic3r::format(_u8L("T%1% -> filament %2%"), physical_head, t.filament_slot_1based);
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -418,6 +418,9 @@ public:
|
||||
bool is_gcode_3mf() { return m_exported_file; }
|
||||
bool only_gcode_mode() { return m_only_gcode; }
|
||||
void set_only_gcode(bool only_gcode) { m_only_gcode = only_gcode; }
|
||||
// The previewed G-code's own settings were loaded into the presets, as a .gcode.3mf's are: the bed already is the
|
||||
// one it was sliced for, and the sidebar shows them.
|
||||
bool gcode_settings_loaded() const { return m_only_gcode && m_gcode_settings_loaded; }
|
||||
|
||||
//BBS: add only gcode mode
|
||||
bool using_exported_file() { return m_exported_file; }
|
||||
@@ -431,6 +434,7 @@ public:
|
||||
bool is_new_project_and_check_state() { return m_new_project_and_check_state; }
|
||||
wxString get_project_name();
|
||||
void update_all_plate_thumbnails(bool force_update = false);
|
||||
void update_plate_thumbnails(int plate_idx, bool force_update);
|
||||
void update_obj_preview_thumbnail(ModelObject *, int obj_idx, int vol_idx, std::vector<Slic3r::ColorRGBA> colors, int camera_view_angle_type);
|
||||
void invalid_all_plate_thumbnails();
|
||||
void force_update_all_plate_thumbnails();
|
||||
@@ -1068,6 +1072,8 @@ public:
|
||||
void mark_plate_toolbar_image_dirty();
|
||||
bool is_plate_toolbar_image_dirty() const;
|
||||
void clear_plate_toolbar_image_dirty();
|
||||
// Bumped each time the plate thumbnails go stale, so each consumer can track it on its own.
|
||||
unsigned plate_toolbar_image_version() const { return m_plate_toolbar_image_version; }
|
||||
|
||||
private:
|
||||
struct priv;
|
||||
@@ -1082,6 +1088,8 @@ private:
|
||||
wxString m_last_loaded_gcode;
|
||||
//BBS: add only gcode mode
|
||||
bool m_only_gcode { false };//just for .gcode file not for .gcode.3mf
|
||||
// Set by load_gcode() for the file it previews, so it means nothing once m_only_gcode is cleared.
|
||||
bool m_gcode_settings_loaded { false };
|
||||
bool m_exported_file { false };
|
||||
bool skip_thumbnail_invalid { false };
|
||||
bool m_loading_project { false };
|
||||
@@ -1090,6 +1098,7 @@ private:
|
||||
int m_valid_plates_count { 0 };
|
||||
int m_check_status = 0; // 0 not check, 1 check success, 2 check failed
|
||||
bool m_b_plate_toolbar_image_dirty{ true };
|
||||
unsigned m_plate_toolbar_image_version{ 0 };
|
||||
|
||||
void suppress_snapshots();
|
||||
void allow_snapshots();
|
||||
|
||||
@@ -299,7 +299,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.display_name = descriptor.name;
|
||||
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
|
||||
descriptor.normalized_error() :
|
||||
(descriptor.description.empty() ? "No description." : descriptor.description);
|
||||
(descriptor.description.empty() ? _u8L("No description.") : descriptor.description);
|
||||
item.author = descriptor.author;
|
||||
item.version = descriptor.version;
|
||||
// The cloud merge overwrites `version` with the latest cloud version, so prefer the preserved local
|
||||
@@ -639,9 +639,9 @@ nlohmann::json PluginsDialog::localized_strings() const
|
||||
{"no_plugins", _u8L("No plugins found")},
|
||||
{"no_match", _u8L("No plugins match \"%s\"")},
|
||||
{"source_mine", _u8L("Mine")},
|
||||
{"source_subscribed", _u8L("Subscribed")},
|
||||
{"source_subscribed", _u8L_CONTEXT("Subscribed", "Plugin")},
|
||||
{"source_orphaned", _u8L("Orphaned")},
|
||||
{"source_local", _u8L("Local")},
|
||||
{"source_local", _u8L_CONTEXT("Local", "Plugin")},
|
||||
{"mixed_capabilities", _u8L("Some plugin capabilities are enabled")},
|
||||
{"collapse_named", _u8L("Collapse %s capabilities")},
|
||||
{"expand_named", _u8L("Expand %s capabilities")},
|
||||
@@ -1094,7 +1094,7 @@ bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const st
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
error = _u8L("Unknown error");
|
||||
}
|
||||
|
||||
if (!downloaded) {
|
||||
@@ -1225,7 +1225,7 @@ void PluginsDialog::update_plugin(const std::string& plugin_key)
|
||||
} catch (const std::exception& ex) {
|
||||
error = ex.what();
|
||||
} catch (...) {
|
||||
error = "Unknown error";
|
||||
error = _u8L("Unknown error");
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
|
||||
@@ -587,7 +587,7 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
|
||||
wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxString tooltip)
|
||||
{
|
||||
|
||||
std::vector<wxString> Regions = {_L("Asia-Pacific"), _L("China"), _L("Europe"), _L("North America"), _L("Others")};
|
||||
std::vector<wxString> Regions = {_L("Asia-Pacific"), _L("China"), _L("Europe"), _L("North America"), _L_CONTEXT("Others", "Region")};
|
||||
std::vector<wxString> local_regions = {"Asia-Pacific", "China", "Europe", "North America", "Others"};
|
||||
|
||||
auto vlist = Regions;
|
||||
@@ -645,7 +645,7 @@ wxBoxSizer *PreferencesDialog::create_item_region_combobox(wxString title, wxStr
|
||||
combobox->SetSelection(current_region);
|
||||
return;
|
||||
} else {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
config->set("region", region.ToStdString());
|
||||
auto area = config->get_country_code();
|
||||
if (agent) {
|
||||
@@ -1435,8 +1435,10 @@ wxBoxSizer *PreferencesDialog::create_item_network_plugin_version(wxString title
|
||||
#ifdef WIN32
|
||||
wxBoxSizer* PreferencesDialog::create_item_link_association( wxString url_prefix, wxString website_name)
|
||||
{
|
||||
wxString title = _L("Associate") + (boost::format(" %1%://") % url_prefix.c_str()).str();
|
||||
wxString tooltip = _L("Associate") + " " + url_prefix + ":// " + _L("with OrcaSlicer so that Orca can open models from") + " " + website_name;
|
||||
// TRN %s is a URL scheme, for example "prusaslicer".
|
||||
wxString title = wxString::Format(_L("Associate %s://"), url_prefix);
|
||||
// TRN The first %s is a URL scheme, for example "prusaslicer"; the second is a website name.
|
||||
wxString tooltip = wxString::Format(_L("Associate %s:// with OrcaSlicer so that Orca can open models from %s"), url_prefix, website_name);
|
||||
|
||||
std::wstring registered_bin; // not used, just here to provide a ref to check fn
|
||||
bool reg_to_current_instance = wxGetApp().check_url_association(url_prefix.ToStdWstring(), registered_bin);
|
||||
@@ -2310,6 +2312,13 @@ void PreferencesDialog::create_items()
|
||||
auto item_show_unsupported = create_item_checkbox(_L("Show unsupported presets"), _L("Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected."), "show_unsupported_presets");
|
||||
g_sizer->Add(item_show_unsupported);
|
||||
|
||||
auto item_export_statistics = create_item_checkbox(_L("Export statistics with G-code"),
|
||||
_L("If enabled, exporting a G-code file also writes its statistics as JSON next to it, as <name>.statistics.json, "
|
||||
"and an exported sliced 3MF file holds them inside: print time, filament and feature totals, "
|
||||
"and the same per layer and per object."),
|
||||
"export_statistics");
|
||||
g_sizer->Add(item_export_statistics);
|
||||
|
||||
//// DEVELOPER > Experimental Features
|
||||
g_sizer->Add(create_item_title(_L("Experimental Features")), 1, wxEXPAND);
|
||||
|
||||
@@ -2479,7 +2488,7 @@ wxBoxSizer* PreferencesDialog::create_debug_page()
|
||||
AppConfig* config = GUI::wxGetApp().app_config;
|
||||
std::string country_code = config->get_country_code();
|
||||
if (agent) {
|
||||
wxGetApp().request_user_logout();
|
||||
wxGetApp().request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
|
||||
agent->set_country_code(country_code);
|
||||
}
|
||||
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"), ConfirmBeforeSendDialog::VisibleButtons::ONLY_CONFIRM); // ORCA VisibleButtons instead ButtonStyle
|
||||
|
||||
@@ -218,7 +218,7 @@ nlohmann::json PresetBundleDialog::localized_strings() const
|
||||
{"update", _u8L("Update")},
|
||||
{"export_presets", _u8L("Export Presets")},
|
||||
{"close", _u8L("Close")},
|
||||
{"open_folder", _u8L("Open folder in explorer")},
|
||||
{"open_folder", _u8L("Show in folder")},
|
||||
{"delete_bundle", _u8L("Delete bundle")},
|
||||
{"unsubscribe_bundle", _u8L("Unsubscribe bundle")},
|
||||
{"update_available", _u8L("Update available")},
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "slic3r/GUI/CalibrationWizardPage.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
@@ -34,14 +36,12 @@
|
||||
#include <wx/image.h>
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/colourdata.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/button.h>
|
||||
#include <wx/statbox.h>
|
||||
#include <wx/colordlg.h>
|
||||
#include <wx/wupdlock.h>
|
||||
#include <wx/menu.h>
|
||||
#include <wx/odcombo.h>
|
||||
@@ -69,6 +69,7 @@
|
||||
#include "MsgDialog.hpp"
|
||||
#include "ParamsDialog.hpp"
|
||||
#include "FilamentPickerDialog.hpp"
|
||||
#include "ColorPickerDialog.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
@@ -84,6 +85,24 @@
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static FilamentColorPickerValue filament_color_picker_value(const DynamicPrintConfig& config, std::size_t index)
|
||||
{
|
||||
FilamentColorPickerValue value;
|
||||
if (const auto* primary = config.opt<ConfigOptionStrings>("filament_colour"); primary && index < primary->values.size())
|
||||
value.primary = primary->values[index];
|
||||
if (const auto* multi = config.opt<ConfigOptionStrings>("filament_multi_colour"); multi && index < multi->values.size()) {
|
||||
std::istringstream stream(multi->values[index]);
|
||||
for (std::string color; stream >> color;)
|
||||
value.colors.push_back(std::move(color));
|
||||
}
|
||||
if (value.colors.empty() && !value.primary.empty())
|
||||
value.colors.push_back(value.primary);
|
||||
if (const auto* types = config.opt<ConfigOptionStrings>("filament_colour_type"); types && index < types->values.size())
|
||||
value.gradient = value.colors.size() > 1 && types->values[index] == "0";
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
#define BORDER_W 10
|
||||
|
||||
// ---------------------------------
|
||||
@@ -911,31 +930,37 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset
|
||||
FilamentColor fila_color = get_cur_color_info();
|
||||
|
||||
// Show filament picker dialog
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type);
|
||||
FilamentPickerDialog dialog(this, fila_id, fila_color, fila_type,
|
||||
filament_color_picker_value(m_preset_bundle->project_config, m_filament_idx));
|
||||
|
||||
if (!dialog.IsDataLoaded()) {
|
||||
// If FilamentPicker fails, fallback to default color picker
|
||||
show_default_color_picker();
|
||||
} else if (dialog.ShowModal() == wxID_OK) {
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else if (dialog.ShowModal() == wxID_OK && dialog.HasSelectionChanged()) {
|
||||
if (const auto& custom = dialog.GetCustomColorSelection()) {
|
||||
const auto value = filament_color_picker_result(*custom);
|
||||
sync_colour_config(value.colors, value.gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
// Get selected filament color data
|
||||
FilamentColor fila_color = dialog.GetSelectedFilamentColor();
|
||||
|
||||
// Check if we have valid color data
|
||||
if (!fila_color.m_colors.empty()) {
|
||||
// Convert to storage format
|
||||
std::vector<std::string> colors;
|
||||
for (const wxColour& color : fila_color.m_colors) {
|
||||
colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
}
|
||||
|
||||
bool is_gradient = (fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR);
|
||||
this->sync_colour_config(colors, is_gradient);
|
||||
} else {
|
||||
// Fallback to basic color if no FilamentColor data
|
||||
wxColour selected_color = dialog.GetSelectedColour();
|
||||
if (selected_color.IsOk()) {
|
||||
std::vector<std::string> color = {selected_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
this->sync_colour_config(color, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1091,30 +1116,8 @@ bool PlaterPresetComboBox::switch_to_tab()
|
||||
|
||||
void PlaterPresetComboBox::change_extruder_color()
|
||||
{
|
||||
// get current color
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour clr(colors->values[m_filament_idx]);
|
||||
if (!clr.IsOk())
|
||||
clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
auto data = new wxColourData();
|
||||
data->SetChooseFull(1);
|
||||
data->SetColour(clr);
|
||||
|
||||
wxColourDialog dialog(this, data);
|
||||
dialog.CenterOnParent();
|
||||
if (dialog.ShowModal() == wxID_OK)
|
||||
{
|
||||
colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
|
||||
DynamicPrintConfig cfg_new = *cfg;
|
||||
cfg_new.set_key_value("filament_colour", colors);
|
||||
|
||||
wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new);
|
||||
this->update();
|
||||
wxGetApp().plater()->on_config_change(cfg_new);
|
||||
}
|
||||
// The Linux sidebar menu edits the same ordered project color as its swatch.
|
||||
show_default_color_picker();
|
||||
}
|
||||
|
||||
void PlaterPresetComboBox::show_add_menu()
|
||||
@@ -1608,19 +1611,12 @@ FilamentColor PlaterPresetComboBox::get_cur_color_info()
|
||||
|
||||
void PlaterPresetComboBox::show_default_color_picker()
|
||||
{
|
||||
DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config;
|
||||
auto colors = static_cast<ConfigOptionStrings*>(cfg->option("filament_colour")->clone());
|
||||
wxColour current_clr(colors->values[m_filament_idx]);
|
||||
if (!current_clr.IsOk())
|
||||
current_clr = wxColour(0, 0, 0); // Don't set alfa to transparence
|
||||
|
||||
m_clrData.SetColour(current_clr);
|
||||
|
||||
wxColourData data = show_sys_picker_dialog(this, m_clrData);
|
||||
if (m_clrData.GetColour() != data.GetColour()) {
|
||||
std::vector<std::string> color = {data.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString()};
|
||||
m_clrData.SetColour(data.GetColour());
|
||||
sync_colour_config(color, false);
|
||||
const auto value = filament_color_picker_value(wxGetApp().preset_bundle->project_config, m_filament_idx);
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(value), clr_picker);
|
||||
if (dialog.ShowModal() == wxID_OK && filament_color_picker_changed(initial, dialog.selection())) {
|
||||
const auto selected = filament_color_picker_result(*dialog.selection());
|
||||
sync_colour_config(selected.colors, selected.gradient);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user