feat: Support 3MF as g-code (use_3mf option) (#14238)
* feat: add support for 3MF file format in printer configurations and export options * fix file extension * enable 3mf for X Max 4 * disable use_3mf for X Plus 4 * Fixed an issue where `label_object_enabled` was not properly propagated to 3mf * enable exclude object for Max 4 * remove hardcoded use 3mf for flashforge, move them to the new printer profiles config
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
|
||||
#include <boost/assign.hpp>
|
||||
#include <boost/bimap.hpp>
|
||||
@@ -8137,6 +8138,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Orca: replicates GCode's object-label sanitization (sanitize_instance_name in GCode.cpp), used to
|
||||
// build the per-instance object name written into slice_info.config so it matches the EXCLUDE_OBJECT /
|
||||
// M486 object name embedded in the g-code. Keep this in sync with GCode.cpp.
|
||||
static std::string sanitize_object_label(const std::string& name)
|
||||
{
|
||||
// Compiled once: building a std::regex is expensive and this runs per object instance.
|
||||
static const std::regex non_word_re("[ !@#$%^&*()=+\\[\\]{};:\",']+");
|
||||
std::string result = std::regex_replace(name, non_word_re, "_");
|
||||
if (!result.empty() && result.front() == '_')
|
||||
result.erase(result.begin());
|
||||
if (!result.empty() && result.back() == '_')
|
||||
result.erase(result.end() - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_slice_info_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config)
|
||||
{
|
||||
std::stringstream stream;
|
||||
@@ -8217,6 +8233,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << "\"/>\n";
|
||||
}
|
||||
|
||||
// Orca: for non-BambuLab printers that label objects in the g-code (Klipper/Marlin/RRF via
|
||||
// EXCLUDE_OBJECT / M486), write the per-instance g-code object name into slice_info.config
|
||||
// (e.g. "OrcaCube_v2.drc_id_0_copy_0") so the printer can correlate objects between the 3MF
|
||||
// and the g-code. The g-code names objects per plate as
|
||||
// "<name>_id_<object index>_copy_<instance index>" (see GCode::set_object_info), which we
|
||||
// reconstruct here from the (sorted) objects_and_instances list. identify_id is unchanged.
|
||||
// BambuLab printers keep the raw object name.
|
||||
const GCodeFlavor slice_gcode_flavor = config.opt_enum<GCodeFlavor>("gcode_flavor");
|
||||
const bool use_gcode_object_name = !GCodeProcessor::s_IsBBLPrinter &&
|
||||
(slice_gcode_flavor == gcfKlipper || slice_gcode_flavor == gcfMarlinLegacy ||
|
||||
slice_gcode_flavor == gcfMarlinFirmware || slice_gcode_flavor == gcfRepRapFirmware);
|
||||
int gcode_object_index = -1;
|
||||
int gcode_copy_index = 0;
|
||||
int last_object_id = -1;
|
||||
|
||||
for (auto it = plate_data->objects_and_instances.begin(); it != plate_data->objects_and_instances.end(); it++)
|
||||
{
|
||||
int obj_id = it->first;
|
||||
@@ -8241,7 +8272,24 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
identify_id = inst->id().id;
|
||||
bool skipped = std::find(plate_data->skipped_objects.begin(), plate_data->skipped_objects.end(), identify_id) !=
|
||||
plate_data->skipped_objects.end();
|
||||
stream << " <" << OBJECT_TAG << " " << IDENTIFYID_ATTR << "=\"" << std::to_string(identify_id) << "\" " << NAME_ATTR << "=\"" << xml_escape(obj->name)
|
||||
|
||||
// Advance the per-plate g-code object/copy index. objects_and_instances is sorted,
|
||||
// so all instances of the same object are contiguous.
|
||||
if (obj_id != last_object_id) {
|
||||
++gcode_object_index;
|
||||
gcode_copy_index = 0;
|
||||
last_object_id = obj_id;
|
||||
} else {
|
||||
++gcode_copy_index;
|
||||
}
|
||||
|
||||
std::string object_name = obj->name;
|
||||
if (use_gcode_object_name)
|
||||
// Matches GCode::get_instance_name(): sanitize(sanitize(name) + "_id_<obj>_copy_<inst>").
|
||||
object_name = sanitize_object_label(sanitize_object_label(obj->name) + "_id_" +
|
||||
std::to_string(gcode_object_index) + "_copy_" + std::to_string(gcode_copy_index));
|
||||
|
||||
stream << " <" << OBJECT_TAG << " " << IDENTIFYID_ATTR << "=\"" << std::to_string(identify_id) << "\" " << NAME_ATTR << "=\"" << xml_escape(object_name)
|
||||
<< "\" " << SKIPPED_ATTR << "=\"" << (skipped ? "true" : "false")
|
||||
<< "\" />\n";
|
||||
}
|
||||
|
||||
@@ -2194,7 +2194,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
|
||||
print->set_done(psGCodeExport);
|
||||
|
||||
if(is_BBL_Printer() && result != nullptr)
|
||||
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
|
||||
// M486), which is driven by exclude_object for every printer
|
||||
if(result != nullptr)
|
||||
result->label_object_enabled = m_enable_exclude_object;
|
||||
// Write the profiler measurements to file
|
||||
PROFILE_UPDATE();
|
||||
|
||||
@@ -1346,7 +1346,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"cooling_tube_retraction",
|
||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
|
||||
"z_offset",
|
||||
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
||||
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
||||
"bed_temperature_formula", "nozzle_flush_dataset"
|
||||
};
|
||||
|
||||
|
||||
@@ -237,7 +237,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"bed_temperature_formula",
|
||||
"filament_notes",
|
||||
"process_notes",
|
||||
"printer_notes"
|
||||
"printer_notes",
|
||||
"use_3mf"
|
||||
};
|
||||
|
||||
static std::unordered_set<std::string> steps_ignore;
|
||||
|
||||
@@ -818,6 +818,14 @@ void PrintConfigDef::init_common_params()
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("use_3mf", coBool);
|
||||
def->label = L("Use 3MF instead of G-code");
|
||||
def->tooltip = L("Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer "
|
||||
"sends the sliced file as a .gcode.3mf, instead of a plain .gcode file.");
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("printer_agent", coString);
|
||||
def->label = L("Printer Agent");
|
||||
def->tooltip = L("Select the network agent implementation for printer communication.");
|
||||
|
||||
@@ -1467,6 +1467,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, enable_filament_ramming))
|
||||
((ConfigOptionBool, tool_change_on_wipe_tower))
|
||||
((ConfigOptionBool, support_multi_bed_types))
|
||||
((ConfigOptionBool, use_3mf))
|
||||
|
||||
// Small Area Infill Flow Compensation
|
||||
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
|
||||
|
||||
@@ -2026,6 +2026,26 @@ wxBoxSizer* MainFrame::create_side_tools()
|
||||
});
|
||||
|
||||
p->append_button(send_gcode_btn);
|
||||
|
||||
// Orca: when the printer accepts a .gcode.3mf (the "Support 3MF as gcode" option),
|
||||
// also offer exporting the sliced .gcode.3mf bundle
|
||||
const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
const auto* use_3mf_opt = printer_config.option<ConfigOptionBool>("use_3mf");
|
||||
if (use_3mf_opt != nullptr && use_3mf_opt->value) {
|
||||
SideButton* export_sliced_file_btn = new SideButton(p, _L("Export plate sliced file"), "");
|
||||
export_sliced_file_btn->SetCornerRadius(0);
|
||||
export_sliced_file_btn->Bind(wxEVT_BUTTON, [this, p](wxCommandEvent&) {
|
||||
m_print_btn->SetLabel(_L("Export plate sliced file"));
|
||||
m_print_select = eExportSlicedFile;
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
fit_tab_labels(); // ORCA on label change
|
||||
p->Dismiss();
|
||||
});
|
||||
p->append_button(export_sliced_file_btn);
|
||||
}
|
||||
|
||||
p->append_button(export_gcode_btn);
|
||||
}
|
||||
else {
|
||||
@@ -4033,6 +4053,13 @@ void MainFrame::set_print_button_to_default(PrintSelectType select_type)
|
||||
m_print_enable = get_enable_print_status() && can_send_gcode();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else if (select_type == PrintSelectType::eExportSlicedFile) {
|
||||
m_print_btn->SetLabel(_L("Export plate sliced file"));
|
||||
m_print_select = eExportSlicedFile;
|
||||
if (m_print_enable)
|
||||
m_print_enable = get_enable_print_status();
|
||||
m_print_btn->Enable(m_print_enable);
|
||||
this->Layout();
|
||||
} else {
|
||||
// unsupport
|
||||
return;
|
||||
|
||||
@@ -2474,7 +2474,11 @@ void Sidebar::update_all_preset_comboboxes()
|
||||
else
|
||||
p->m_bpButton_ams_filament->Hide();
|
||||
|
||||
auto print_btn_type = MainFrame::PrintSelectType::eExportGcode;
|
||||
// Orca: with "Support 3MF as gcode" (use_3mf) the local export is a .gcode.3mf bundle, so when no
|
||||
// printer host/IP is configured the default action is "Export plate sliced file" (mirrors the
|
||||
// print dropdown) instead of "Export G-code file".
|
||||
auto print_btn_type = cfg.opt_bool("use_3mf") ? MainFrame::PrintSelectType::eExportSlicedFile
|
||||
: MainFrame::PrintSelectType::eExportGcode;
|
||||
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
|
||||
wxString apikey;
|
||||
if(url.empty())
|
||||
@@ -10229,7 +10233,7 @@ void Plater::priv::on_action_print_plate(SimpleEvent&)
|
||||
m_select_machine_dlg->prepare(partplate_list.get_curr_plate_index());
|
||||
m_select_machine_dlg->ShowModal();
|
||||
} else {
|
||||
q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr, true);
|
||||
q->send_gcode_legacy(PLATE_CURRENT_IDX, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10330,7 +10334,7 @@ void Plater::priv::on_action_print_all(SimpleEvent&)
|
||||
m_select_machine_dlg->prepare(PLATE_ALL_IDX);
|
||||
m_select_machine_dlg->ShowModal();
|
||||
} else {
|
||||
q->send_gcode_legacy(PLATE_ALL_IDX, nullptr, true);
|
||||
q->send_gcode_legacy(PLATE_ALL_IDX, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16093,7 +16097,7 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &
|
||||
// and let the background processing start.
|
||||
this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART);
|
||||
}
|
||||
void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool use_3mf)
|
||||
void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn)
|
||||
{
|
||||
// if physical_printer is selected, send gcode for this printer
|
||||
// DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config();
|
||||
@@ -16105,17 +16109,9 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
|
||||
if (upload_job.empty())
|
||||
return;
|
||||
|
||||
const auto host_type_opt = physical_printer_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
|
||||
const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink;
|
||||
const auto* ff_serial_opt = physical_printer_config->option<ConfigOptionString>("flashforge_serial_number");
|
||||
const auto* ff_code_opt = physical_printer_config->option<ConfigOptionString>("printhost_apikey");
|
||||
const bool flashforge_local_api =
|
||||
host_type == htFlashforge &&
|
||||
ff_serial_opt != nullptr && !ff_serial_opt->value.empty() &&
|
||||
ff_code_opt != nullptr && !ff_code_opt->value.empty();
|
||||
|
||||
if (flashforge_local_api)
|
||||
use_3mf = true;
|
||||
// Orca: the use_3mf printer option makes us send a .gcode.3mf to the printer
|
||||
const auto* use_3mf_opt = physical_printer_config->option<ConfigOptionBool>("use_3mf");
|
||||
const bool use_3mf = use_3mf_opt != nullptr && use_3mf_opt->value;
|
||||
|
||||
upload_job.upload_data.use_3mf = use_3mf;
|
||||
|
||||
@@ -16139,7 +16135,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
|
||||
}
|
||||
default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string()));
|
||||
if (use_3mf) {
|
||||
default_output_file.replace_extension("3mf");
|
||||
// Orca: a gcode-in-3mf bundle is named ".gcode.3mf" (matching "Export plate sliced file")
|
||||
default_output_file.replace_extension(".gcode.3mf");
|
||||
}
|
||||
|
||||
// Repetier specific: Query the server for the list of file groups.
|
||||
@@ -16166,6 +16163,13 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
|
||||
auto preset_bundle = wxGetApp().preset_bundle;
|
||||
auto config = get_app_config();
|
||||
|
||||
const auto host_type_opt = physical_printer_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
|
||||
const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink;
|
||||
const auto* ff_serial_opt = physical_printer_config->option<ConfigOptionString>("flashforge_serial_number");
|
||||
const auto* ff_code_opt = physical_printer_config->option<ConfigOptionString>("printhost_apikey");
|
||||
const bool flashforge_local_api = host_type == htFlashforge && ff_serial_opt != nullptr && !ff_serial_opt->value.empty() &&
|
||||
ff_code_opt != nullptr && !ff_code_opt->value.empty();
|
||||
|
||||
std::unique_ptr<PrintHostSendDialog> pDlg;
|
||||
if (host_type == htElegooLink) {
|
||||
pDlg = std::make_unique<ElegooPrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
|
||||
|
||||
@@ -522,7 +522,7 @@ public:
|
||||
/* -1: send current gcode if not specified
|
||||
* -2: send all gcode to target machine */
|
||||
int send_gcode(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
|
||||
void send_gcode_legacy(int plate_idx = -1, Export3mfProgressFn proFn = nullptr, bool use_3mf = false);
|
||||
void send_gcode_legacy(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
|
||||
int export_config_3mf(int plate_idx = -1, Export3mfProgressFn proFn = nullptr);
|
||||
//BBS jump to nonitor after print job finished
|
||||
void send_calibration_job_finished(wxCommandEvent &evt);
|
||||
|
||||
@@ -4477,6 +4477,7 @@ void TabPrinter::build_fff()
|
||||
optgroup->append_single_option_line("gcode_flavor", "printer_basic_information_advanced#g-code-flavor");
|
||||
optgroup->append_single_option_line("pellet_modded_printer", "printer_basic_information_advanced#pellet-modded-printer");
|
||||
optgroup->append_single_option_line("bbl_use_printhost", "printer_basic_information_advanced#use-3rd-party-print-host");
|
||||
optgroup->append_single_option_line("use_3mf");
|
||||
optgroup->append_single_option_line("scan_first_layer" , "printer_basic_information_advanced#scan-first-layer");
|
||||
optgroup->append_single_option_line("enable_power_loss_recovery", "printer_basic_information_advanced#power-loss-recovery");
|
||||
//option = optgroup->get_option("wrapping_exclude_area");
|
||||
|
||||
Reference in New Issue
Block a user