Add support for Draco (.drc) format (#10681)

* Add read support for Google's Draco (.drc) format.

* Fix build on Linux

* Use boost instead of fstat.

* Switch to boost memory-mapped file to save RAM and potentially improve performance.

* Trim trailing whitespace.

* Initial Draco write support.

Currently always exports with 16-bit precision and speed 0 (best compression).
The back-end function does have arguments to specify them, it's just not hooked into the GUI.

* Add Draco to the About dialogue.

* Fix Linux compile (hopefully)

* Add an option to associate DRC files on Windows.

* Implement a Preferences option to set Draco position quantization bits

* Update src/slic3r/GUI/Preferences.cpp

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>

* Some slight changes to ianalexis's suggestion.

* Implement a create_item_spinctrl() function for numeric inputs, and use that instead of create_item_input().

* Move "bits" to inside the spinctrl box.

* Refactor following yw4z's feedback

* Update src/slic3r/GUI/Preferences.cpp

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>

* Change to 0 bits as the default setting for Draco export precision.

* Change to a lossy checkbox and a bits field with a range of 8-30.

* Proper SpinInput code from yw4z

* Revert "Proper SpinInput code from yw4z"

This reverts commit 7e9c85f31a.

* Revert "Change to a lossy checkbox and a bits field with a range of 8-30."

This reverts commit d642c9bcc0.

* Redo preferences based on SoftFever's feedback

* Refactor to minimize code duplication

* Fix padding

* Improve Draco export quality level tooltip clarity

Clarify that 0 means lossless compression (not uncompressed),
document the valid lossy range (8-30), and better explain the
tradeoff between file size and geometric detail.

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
Maeyanie
2026-02-06 05:27:17 -05:00
committed by GitHub
parent dffaa5c0a9
commit bb30999673
18 changed files with 396 additions and 12 deletions

View File

@@ -90,6 +90,7 @@ void CopyrightsDialog::fill_entries()
{ "CGAL", "", "https://www.cgal.org" },
{ "Clipper", "", "http://www.angusj.co" },
{ "libcurl", "", "https://curl.se/libcurl" },
{ "Draco", "", "https://google.github.io/draco/" },
{ "Eigen3", "", "http://eigen.tuxfamily.org" },
{ "Expat", "", "http://www.libexpat.org" },
{ "fast_float", "", "https://github.com/fastfloat/fast_float" },

View File

@@ -537,10 +537,10 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
/* FT_GCODE */ { "G-code files"sv, { ".gcode"sv} },
#ifdef __APPLE__
/* FT_MODEL */
{"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv}},
{"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".usd"sv, ".usda"sv, ".usdc"sv, ".usdz"sv, ".abc"sv, ".ply"sv, ".drc"sv}},
#else
/* FT_MODEL */
{"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv}},
{"Supported files"sv, {".3mf"sv, ".stl"sv, ".oltp"sv, ".stp"sv, ".step"sv, ".svg"sv, ".amf"sv, ".obj"sv, ".drc"sv}},
#endif
/* FT_ZIP */ { "ZIP files"sv, { ".zip"sv } },
/* FT_PROJECT */ { "Project files"sv, { ".3mf"sv} },
@@ -550,6 +550,7 @@ static const FileWildcards file_wildcards_by_type[FT_SIZE] = {
/* FT_SVG */ { "SVG files"sv, { ".svg"sv } },
/* FT_TEX */ { "Texture"sv, { ".png"sv, ".svg"sv } },
/* FT_SL1 */ { "Masked SLA files"sv, { ".sl1"sv, ".sl1s"sv } },
/* FT_DRC */ { "Draco files"sv, { ".drc"sv } },
};
// This function produces a Win32 file dialog file template mask to be consumed by wxWidgets on all platforms.

View File

@@ -106,6 +106,8 @@ enum FileType
FT_SL1,
FT_DRC,
FT_SIZE,
};

View File

@@ -863,6 +863,27 @@ void MenuFactory::append_menu_item_export_stl(wxMenu* menu, bool is_mulity_menu)
}, m_parent);
}
void MenuFactory::append_menu_item_export_drc(wxMenu* menu, bool is_mulity_menu)
{
append_menu_item(menu, wxID_ANY, _L("Export as one DRC") + dots, "",
[](wxCommandEvent&) { plater()->export_stl(false, true, false, FT_DRC); }, "", nullptr,
[is_mulity_menu]() {
const Selection& selection = plater()->canvas3D()->get_selection();
if (is_mulity_menu)
return selection.is_multiple_full_instance() || selection.is_multiple_full_object();
else
return selection.is_single_full_instance() || selection.is_single_full_object();
}, m_parent);
if (!is_mulity_menu)
return;
append_menu_item(menu, wxID_ANY, _L("Export as DRCs") + dots, "",
[](wxCommandEvent&) { plater()->export_stl(false, true, true, FT_DRC); }, "", nullptr,
[]() {
const Selection& selection = plater()->canvas3D()->get_selection();
return selection.is_multiple_full_instance() || selection.is_multiple_full_object();
}, m_parent);
}
void MenuFactory::append_menu_item_reload_from_disk(wxMenu* menu)
{
append_menu_item(menu, wxID_ANY, _L("Reload from disk"), _L("Reload the selected parts from disk"),
@@ -1282,6 +1303,7 @@ void MenuFactory::create_common_object_menu(wxMenu* menu)
append_menu_item_reload_from_disk(menu);
append_menu_item_export_stl(menu);
append_menu_item_export_drc(menu);
// "Scale to print volume" makes a sense just for whole object
append_menu_item_scale_selection_to_fit_print_volume(menu);
@@ -1367,6 +1389,7 @@ void MenuFactory::create_extra_object_menu()
append_menu_item_replace_with_stl(&m_object_menu);
append_menu_item_replace_all_with_stl(&m_object_menu);
append_menu_item_export_stl(&m_object_menu);
append_menu_item_export_drc(&m_object_menu);
}
void MenuFactory::create_bbl_assemble_object_menu()
@@ -1401,6 +1424,7 @@ void MenuFactory::create_part_menu()
append_menu_item_delete(menu);
append_menu_item_reload_from_disk(menu);
append_menu_item_export_stl(menu);
append_menu_item_export_drc(menu);
append_menu_item_fix_through_netfabb(menu);
append_menu_items_mirror(menu);
append_menu_item_merge_parts_to_single_part(menu);
@@ -1782,6 +1806,7 @@ wxMenu* MenuFactory::multi_selection_menu()
append_menu_item_change_filament(menu);
menu->AppendSeparator();
append_menu_item_export_stl(menu, true);
append_menu_item_export_drc(menu, true);
}
else {
append_menu_item_center(menu);

View File

@@ -141,6 +141,7 @@ private:
wxMenuItem* append_menu_item_fix_through_netfabb(wxMenu* menu);
//wxMenuItem* append_menu_item_simplify(wxMenu* menu);
void append_menu_item_export_stl(wxMenu* menu, bool is_mulity_menu = false);
void append_menu_item_export_drc(wxMenu* menu, bool is_mulity_menu = false);
void append_menu_item_reload_from_disk(wxMenu* menu);
void append_menu_item_replace_with_stl(wxMenu* menu);
void append_menu_item_replace_all_with_stl(wxMenu* menu);

View File

@@ -2516,6 +2516,12 @@ void MainFrame::init_menubar_as_editor()
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as STLs") + dots, _L("Export all objects as STLs"),
[this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(false, false, true); }, "menu_export_stl", nullptr,
[this](){return can_export_model(); }, this);
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as one DRC") + dots, _L("Export all objects as one DRC"),
[this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(false, false, false, FT_DRC); }, "menu_export_stl", nullptr,
[this](){return can_export_model(); }, this);
append_menu_item(export_menu, wxID_ANY, _L("Export all objects as DRCs") + dots, _L("Export all objects as DRCs"),
[this](wxCommandEvent&) { if (m_plater) m_plater->export_stl(false, false, true, FT_DRC); }, "menu_export_stl", nullptr,
[this](){return can_export_model(); }, this);
append_menu_item(export_menu, wxID_ANY, _L("Export Generic 3MF") + dots/* + "\t" + ctrl + "G"*/, _L("Export 3MF file without using some 3mf-extensions"),
[this](wxCommandEvent&) { if (m_plater) m_plater->export_core_3mf(); }, "menu_export_sliced_file", nullptr,
[this](){return can_export_model(); }, this);

View File

@@ -49,6 +49,7 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/Format/STEP.hpp"
#include "libslic3r/Format/AMF.hpp"
//#include "libslic3r/Format/3mf.hpp"
@@ -6967,6 +6968,7 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
wxString wildcard;
switch (file_type) {
case FT_STL:
case FT_DRC:
case FT_AMF:
case FT_3MF:
case FT_GCODE:
@@ -6988,6 +6990,12 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
dlg_title = _L("Export STL file:");
break;
}
case FT_DRC:
{
output_file.replace_extension("drc");
dlg_title = _L("Export Draco file:");
break;
}
case FT_AMF:
{
// XXX: Problem on OS X with double extension?
@@ -13685,7 +13693,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
//BBS: remove GCodeViewer as seperate APP logic
bool Plater::load_files(const wxArrayString& filenames)
{
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip)", std::regex::icase);
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase);
const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
std::vector<fs::path> normal_paths;
@@ -14722,10 +14730,22 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st
// BBS export with/without boolean, however, stil merge mesh
#define EXPORT_WITH_BOOLEAN 0
void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, FileType file_type)
{
if (p->model.objects.empty()) { return; }
int quality = 0;
switch (file_type) {
case FT_DRC:
AppConfig* app_config = wxGetApp().app_config;
if (app_config)
quality = stoi(app_config->get("drc_bits"));
else
quality = DRC_BITS_DEFAULT;
break;
}
wxString path;
if (multi_stls) {
wxDirDialog dlg(this, _L("Choose a directory"), from_u8(wxGetApp().app_config->get_last_dir()),
@@ -14734,7 +14754,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
path = dlg.GetPath() + "/";
}
} else {
path = p->get_export_file(FT_STL);
path = p->get_export_file(file_type);
}
if (path.empty()) { return; }
const std::string path_u8 = into_u8(path);
@@ -14881,11 +14901,17 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
else
mesh_to_export = mesh_to_export_sla;
auto get_save_file = [](std::string const & dir, std::string const & name) {
auto path = dir + name + ".stl";
auto get_save_file = [file_type](std::string const & dir, std::string const & name) {
std::string ext = "";
switch (file_type) {
case FT_STL: ext = ".stl"; break;
case FT_DRC: ext = ".drc"; break;
}
auto path = dir + name + ext;
int n = 1;
while (boost::filesystem::exists(path))
path = dir + name + "(" + std::to_string(n++) + ").stl";
path = dir + name + "(" + std::to_string(n++) + ")"+ext;
return path;
};
@@ -14918,7 +14944,10 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
auto mesh = mesh_to_export(*object, i.second);
mesh.translate(-object->origin_translation.cast<float>());
Slic3r::store_stl(get_save_file(path_u8, object->name).c_str(), &mesh, true);
switch (file_type) {
case FT_STL: Slic3r::store_stl(get_save_file(path_u8, object->name).c_str(), &mesh, true); break;
case FT_DRC: Slic3r::store_drc(get_save_file(path_u8, object->name).c_str(), &mesh, quality); break;
}
}
return;
}
@@ -14931,12 +14960,19 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls)
for (const ModelObject* o : p->model.objects) {
auto mesh = mesh_to_export(*o, -1);
mesh.translate(-o->origin_translation.cast<float>());
Slic3r::store_stl(get_save_file(path_u8, o->name).c_str(), &mesh, true);
switch (file_type) {
case FT_STL: Slic3r::store_stl(get_save_file(path_u8, o->name).c_str(), &mesh, true); break;
case FT_DRC: Slic3r::store_drc(get_save_file(path_u8, o->name).c_str(), &mesh, quality); break;
}
}
return;
}
Slic3r::store_stl(path_u8.c_str(), &mesh, true);
switch (file_type) {
case FT_STL: Slic3r::store_stl(path_u8.c_str(), &mesh, true); break;
case FT_DRC: Slic3r::store_drc(path_u8.c_str(), &mesh, quality); break;
}
}
//BBS: remove amf export

View File

@@ -485,7 +485,7 @@ public:
void send_gcode_finish(wxString name);
void export_core_3mf();
static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function<void(const std::string&)> notify_func = {});
void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false);
void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL);
//BBS: remove amf
//void export_amf();
//BBS add extra param for exporting 3mf silence

View File

@@ -6,6 +6,7 @@
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -750,6 +751,58 @@ wxBoxSizer *PreferencesDialog::create_item_auto_reslice(wxString title, wxString
return sizer_row;
}
wxBoxSizer* PreferencesDialog::create_item_draco(wxString title, wxString side_label, wxString tooltip)
{
wxBoxSizer* sizer_input = new wxBoxSizer(wxHORIZONTAL);
auto input_title = new wxStaticText(m_parent, wxID_ANY, title, wxDefaultPosition, DESIGN_TITLE_SIZE, wxST_NO_AUTORESIZE);
input_title->SetForegroundColour(DESIGN_GRAY900_COLOR);
input_title->SetFont(::Label::Body_14);
input_title->SetToolTip(tooltip);
input_title->Wrap(DESIGN_TITLE_SIZE.x);
input_title->SetToolTip(tooltip);
auto input = new ::TextInput(m_parent, wxEmptyString, side_label, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER);
StateColor input_bg(std::pair<wxColour, int>(wxColour("#F0F0F1"), StateColor::Disabled),
std::pair<wxColour, int>(*wxWHITE, StateColor::Enabled));
input->SetBackgroundColor(input_bg);
input->GetTextCtrl()->SetValue(app_config->get("drc_bits"));
wxTextValidator validator(wxFILTER_DIGITS);
input->SetToolTip(tooltip);
input->GetTextCtrl()->SetValidator(validator);
sizer_input->AddSpacer(FromDIP(DESIGN_LEFT_MARGIN));
sizer_input->Add(input_title, 0, wxALIGN_CENTER_VERTICAL);
sizer_input->Add(input , 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
std::function<void()> set_draco_bits = [this, input]() {
long drc_bits = DRC_BITS_DEFAULT;
input->GetTextCtrl()->GetValue().ToLong(&drc_bits);
if (drc_bits > DRC_BITS_MAX) {
drc_bits = DRC_BITS_MAX;
input->GetTextCtrl()->SetValue(std::to_string(drc_bits));
} else if (drc_bits < DRC_BITS_MIN && drc_bits != 0) {
drc_bits = DRC_BITS_MIN;
input->GetTextCtrl()->SetValue(std::to_string(drc_bits));
}
app_config->set("drc_bits", std::to_string(drc_bits));
app_config->save();
};
input->GetTextCtrl()->Bind(wxEVT_TEXT_ENTER, [set_draco_bits](wxCommandEvent& e) {
set_draco_bits();
e.Skip();
});
input->GetTextCtrl()->Bind(wxEVT_KILL_FOCUS, [set_draco_bits](wxFocusEvent& e) {
set_draco_bits();
e.Skip();
});
return sizer_input;
}
wxBoxSizer* PreferencesDialog::create_item_darkmode(wxString title,wxString tooltip, std::string param)
{
wxBoxSizer* m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL);
@@ -864,6 +917,15 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
}
}
if (param == "associate_drc") {
bool pbool = app_config->get("associate_drc") == "true" ? true : false;
if (pbool) {
wxGetApp().associate_files(L"drc");
} else {
wxGetApp().disassociate_files(L"drc");
}
}
if (param == "associate_stl") {
bool pbool = app_config->get("associate_stl") == "true" ? true : false;
if (pbool) {
@@ -1306,6 +1368,13 @@ void PreferencesDialog::create_items()
g_sizer->Add(item_pop_up_filament_map_dialog);
#endif
auto item_draco_bits = create_item_draco(_L("Quality level for Draco export"),
_L("bits"),
_L("Controls the quantization bit depth used when compressing the mesh to Draco format.\n"
"0 = lossless compression (geometry is preserved at full precision). Valid lossy values range from 8 to 30.\n"
"Lower values produce smaller files but lose more geometric detail; higher values preserve more detail at the cost of larger files."));
g_sizer->Add(item_draco_bits);
g_sizer->AddSpacer(FromDIP(10));
sizer_page->Add(g_sizer, 0, wxEXPAND);
@@ -1554,6 +1623,9 @@ void PreferencesDialog::create_items()
auto item_associate_3mf = create_item_checkbox(_L("Associate 3MF files to OrcaSlicer"), _L("If enabled, sets OrcaSlicer as default application to open 3MF files.") , "associate_3mf");
g_sizer->Add(item_associate_3mf);
auto item_associate_drc = create_item_checkbox(_L("Associate DRC files to OrcaSlicer"), _L("If enabled, sets OrcaSlicer as default application to open DRC files."), "associate_drc");
g_sizer->Add(item_associate_drc);
auto item_associate_stl = create_item_checkbox(_L("Associate STL files to OrcaSlicer"), _L("If enabled, sets OrcaSlicer as default application to open STL files.") , "associate_stl");
g_sizer->Add(item_associate_stl);

View File

@@ -91,9 +91,11 @@ public:
wxBoxSizer *create_item_button(wxString title, wxString title2, wxString tooltip, wxString tooltip2, std::function<void()> onclick);
wxBoxSizer *create_item_downloads(wxString title, wxString tooltip);
wxBoxSizer *create_item_input(wxString title, wxString title2, wxString tooltip, std::string param, std::function<void(wxString)> onchange = {});
wxBoxSizer *create_item_spinctrl(wxString title, wxString title2, wxString side_label, wxString tooltip, std::string param, int min, int max, std::function<void(int)> onchange = nullptr);
wxBoxSizer *create_camera_orbit_mult_input(wxString title, wxString tooltip);
wxBoxSizer *create_item_backup(wxString title, wxString tooltip);
wxBoxSizer *create_item_auto_reslice(wxString title, wxString checkbox_tooltip, wxString delay_tooltip);
wxBoxSizer *create_item_draco(wxString title, wxString side_label, wxString tooltip);
wxBoxSizer *create_item_multiple_combobox(wxString title, wxString tooltip, std::string parama, std::vector<wxString> vlista, std::vector<wxString> vlistb);
#ifdef WIN32
wxBoxSizer *create_item_link_association(wxString url_prefix, wxString website_name);