Compare commits

...

22 Commits

Author SHA1 Message Date
Hanif Koh
2a2fe3d660 Use PartPlate's m_height to allow CLI to perform proper BuildVolume check 2026-08-28 11:57:13 +08:00
Hanif Koh
9f8d943b05 Fix exported first_layer_time. Fixes GitHub Issue #14740 2026-08-28 11:57:13 +08:00
Hanif Koh
b0554a9447 Fix OOB heap write from stale filament_self_index on --load-filaments. Fixes Github Issue #14181 2026-08-28 11:57:13 +08:00
Hanif Koh
88b9ad3d81 Fix CLI wipe-tower position silently reused across plates when the array is undersized 2026-08-28 11:57:13 +08:00
Hanif Koh
b2836c48ac Read Nozzle Height and Align Center in Arrange Config 2026-08-28 11:57:13 +08:00
Hanif Koh
334b8acdd1 Fix incorrect early exit for CLI mode no-support preventing parameters from being read 2026-08-28 11:57:13 +08:00
Hanif Koh
cd94c46df2 Update Option Type for LogFile argument 2026-08-28 11:57:13 +08:00
Hanif Koh
3256e20865 Reject invalid CLI argument values instead of silently accepting them
read_cli()'s coBool branch discarded deserialize()'s return value, so an
invalid value like --allow-rotations=false silently left the previous/
default value in place instead of erroring, unlike every other option
type. The vector branch (coBools/coFloats/coInts/coPercents/
coFloatsOrPercents) had the same gap, plus a worse failure mode: passing
"nil" into a non-nullable vector option (the common case - only options
explicitly marked nullable tolerate it) throws Slic3r::ConfigurationError
instead of returning false, which was uncaught and aborted the whole
process (e.g. --nozzle-diameter=nil, --filament-soluble=nil). Both
branches now check/catch and report "Invalid value for option --X" the
same way the existing generic scalar branch already does.

Also corrected --help: six boolean CLI flags (load_defaultfila, min_save,
normative_check, skip_modified_gcodes, allow_newer_file, allow_mix_temp)
carried a "option" hint implying a space-separated argument they never
actually consume (coBool/coBools flags only take a value via --flag=0/1,
never a following token) - removed it to match how every other plain
bool flag is already declared. And fixed a --load_settings/
--load_filaments typo (underscores) in --help's own footer; the real
flags are dash-separated, as shown directly above it in the same output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCuPoRbBMoPxKpcYZmfdan
2026-08-28 11:57:13 +08:00
Hanif Koh
13d7bda6f8 Use printable area bounding box instead of naive vertex calculation. Fixes Github issue #15363 2026-08-28 11:57:13 +08:00
Hanif Koh
739ae609aa Safeguard CLI mode get_at() config functions to prevent crash from missing filament_id 2026-08-28 11:57:13 +08:00
Hanif Koh
d5fb17e563 Create Directories Recursively, and early exit gracefully if failed to create 2026-08-28 11:57:13 +08:00
Kris Austin
6d1584844e fix: STEP part names with accented characters import as numbers (clears 6 warnings) (#15406) 2026-08-27 19:06:06 -03:00
schneider007
6fdd4945c1 Fix bug: centroid calculation (#15399) 2026-08-27 08:16:34 -03:00
Kris Austin
cbd1bf2c37 build: clear 295 more -Woverloaded-virtual warnings in GUI widgets (#15394)
build: clear 295 -Woverloaded-virtual warnings in GUI widgets

Turns three hidden base virtuals into real overrides, clearing 295 of
the 553 -Woverloaded-virtual warnings and taking a full clang-cl build
from 1,264 to 969. Part of #15374.

Search.hpp: SearchDialog::Popup and SearchObjectDialog::Popup took a
wxPoint that neither body ever read, hiding the virtual
wxPopupTransientWindow::Popup(wxWindow*). Both bodies clear the input,
call the base, set focus and refill the list, and SearchObjectDialog
also guards re-entry, so hiding meant none of that ran when the window
was popped through a base pointer. They now override and forward focus.

LabeledStaticBox::SetFont and ScrolledWindow::SetBackgroundColour hid
their base virtuals the same way, so the label metrics recompute and
the child colour propagation only ran for callers holding the concrete
type. Both now override.

Marking a member override makes clang flag every other unmarked
override in the same class, so seven sibling declarations needed the
keyword too. Left unmarked they were worth 481 warnings, which would
have made this a net loss.

MSWDismissUnfocusedPopup is declared only inside #ifdef __WXMSW__ in
wx/popupwin.h, so off Windows there is no base virtual to override and
the keyword would not compile. Both the declarations and the definitions
are guarded, which is how wxWidgets itself declares MSWWindowProc in
wx/nativewin.h and how this repo already handles it in BBLTopbar,
MainFrame, Button, ComboBox and TabCtrl.

ScrolledWindow's constructor left m_userPanel and m_scroll_win
uninitialised unless the style requested a vertical scrollbar, while
SetBackgroundColour dereferences both. No caller hits that today since
every instantiation passes wxVSCROLL, but the override widens who can
reach them, so they are now initialised alongside their siblings.

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-08-27 08:06:50 -03:00
Ian Chua
4c71ec4770 feat(plugin): storage API (#14923)
# Description

Exposes the filepath for the current plugin's directory via
```python
orca.host.plugin.storage
```
Tested with:

[test_storage_api_any.py](https://github.com/user-attachments/files/30333105/test_storage_api_any.py)

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-27 14:52:09 +08:00
Ian Chua
3ab5157a49 Merge branch 'main' into feat/plugin-storage-api 2026-08-27 14:52:00 +08:00
Ian Chua
390e47a8c6 Merge branch 'main' into feat/plugin-storage-api 2026-08-25 13:49:04 +08:00
Ian Chua
58b17f08e0 Merge branch 'main' into feat/plugin-storage-api 2026-08-14 15:19:26 +08:00
Ian Chua
274150af27 Merge branch 'main' into feat/plugin-storage-api 2026-08-05 20:11:29 +08:00
Ian Chua
2e246341d1 move the storage directory outside the actual plugin code folder 2026-07-24 18:57:46 +08:00
Ian Chua
9513300835 Merge branch 'main' into feat/plugin-storage-api 2026-07-24 13:09:39 +08:00
Ian Chua
f7caf0db07 feat(plugin): storage API 2026-07-23 21:04:08 +08:00
24 changed files with 1590 additions and 97 deletions

View File

@@ -1793,8 +1793,9 @@ int CLI::run(int argc, char **argv)
old_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values;
old_exclude_area = config.option<ConfigOptionPoints>("bed_exclude_area", true)->values;
if (old_printable_area.size() >= 4) {
old_printable_width = (int)(old_printable_area[2].x() - old_printable_area[0].x());
old_printable_depth = (int)(old_printable_area[2].y() - old_printable_area[0].y());
BoundingBoxf old_printable_bbox(old_printable_area);
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
}
old_printable_height = (int)(config.opt_float("printable_height"));
@@ -2343,8 +2344,9 @@ int CLI::run(int argc, char **argv)
Pointfs orig_printable_area;
orig_printable_area = config.option<ConfigOptionPoints>("printable_area", true)->values;
if (orig_printable_area.size() >= 4) {
orig_printable_width = (int)(orig_printable_area[2].x() - orig_printable_area[0].x());
orig_printable_depth = (int)(orig_printable_area[2].y() - orig_printable_area[0].y());
BoundingBoxf orig_printable_bbox(orig_printable_area);
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
}
orig_printable_height = (int)(config.opt_float("printable_height"));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
@@ -3138,7 +3140,22 @@ int CLI::run(int argc, char **argv)
std::vector<int> old_variant_counts(filament_count, 1), new_variant_counts;
ConfigOptionInts* filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index");
if (!filament_self_index_opt) {
bool need_regenerate_self_index = !filament_self_index_opt;
if (filament_self_index_opt) {
// a filament_self_index carried over from a project with a different
// filament_count can imply more distinct filament groups than currently exist.
// old_start_indice/old_variant_counts below are sized to filament_count, so an
// unreconciled index walks old_start_indice[++k] past its bounds (heap corruption).
int max_self_index = 0;
for (int v : filament_self_index_opt->values)
max_self_index = std::max(max_self_index, v);
if (max_self_index > filament_count) {
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index implies %1% filament groups but filament_count is %2%, regenerating")
% max_self_index % filament_count;
need_regenerate_self_index = true;
}
}
if (need_regenerate_self_index) {
filament_self_index_opt = m_print_config.option<ConfigOptionInts>("filament_self_index", true);
std::vector<int>& filament_self_indice = filament_self_index_opt->values;
filament_self_indice.resize(filament_count);
@@ -3732,6 +3749,8 @@ int CLI::run(int argc, char **argv)
double height_to_lid = m_print_config.opt_float("extruder_clearance_height_to_lid");
double height_to_rod = m_print_config.opt_float("extruder_clearance_height_to_rod");
double clearance_radius = m_print_config.opt_float("extruder_clearance_radius");
double nozzle_height = m_print_config.opt_float("nozzle_height");
Vec2d align_center = m_print_config.option<ConfigOptionPoint>("best_object_pos")->value;
int shared_printable_width = 0, shared_printable_depth = 0, shared_printable_height = 0, shared_center_x = 0, shared_center_y = 0;
//double plate_stride;
std::string bed_texture;
@@ -3742,8 +3761,11 @@ int CLI::run(int argc, char **argv)
if (m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")) {
current_extruder_print_heights = m_print_config.opt<ConfigOptionFloatsNullable>("extruder_printable_height")->values;
}
current_printable_width = current_printable_area[2].x() - current_printable_area[0].x();
current_printable_depth = current_printable_area[2].y() - current_printable_area[0].y();
{
BoundingBoxf current_printable_bbox(current_printable_area);
current_printable_width = static_cast<int>(current_printable_bbox.size().x());
current_printable_depth = static_cast<int>(current_printable_bbox.size().y());
}
current_printable_height = print_height;
if (old_printable_width == 0)
old_printable_width = current_printable_width;
@@ -3944,6 +3966,13 @@ int CLI::run(int argc, char **argv)
ConfigOptionFloats *wipe_x_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_x"));
ConfigOptionFloats *wipe_y_option = dynamic_cast<ConfigOptionFloats *>(print_config.option("wipe_tower_y"));
// get_at() clamps an out-of-range index to entry 0 instead of erroring, which
// would silently reuse another plate's wipe tower position here. Warn so a mismatched
// wipe_tower_x/y array (e.g. from a project saved before this plate was added) is visible.
if (static_cast<size_t>(plate_index) >= wipe_x_option->values.size() || static_cast<size_t>(plate_index) >= wipe_y_option->values.size()) {
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: wipe_tower_x/y only has %2%/%3% entries, reusing entry 0's position")
%(plate_index+1) %wipe_x_option->values.size() %wipe_y_option->values.size();
}
plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index);
plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index);
@@ -4139,8 +4168,9 @@ int CLI::run(int argc, char **argv)
temp_extruder_print_heights = config.option<ConfigOptionFloatsNullable>("extruder_printable_height", true)->values;
if (temp_printable_area.size() >= 4) {
printer_plate.printable_width = (int)(temp_printable_area[2].x() - temp_printable_area[0].x());
printer_plate.printable_depth = (int)(temp_printable_area[2].y() - temp_printable_area[0].y());
BoundingBoxf temp_printable_bbox(temp_printable_area);
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
}
if (temp_exclude_area.size() >= 4) {
@@ -4788,6 +4818,8 @@ int CLI::run(int argc, char **argv)
arrange_cfg.clearance_height_to_rod = height_to_rod;
arrange_cfg.clearance_height_to_lid = height_to_lid;
arrange_cfg.clearance_radius = clearance_radius;
arrange_cfg.nozzle_height = nozzle_height;
arrange_cfg.align_center = align_center;
arrange_cfg.printable_height = print_height;
arrange_cfg.min_obj_distance = 0;
if (arrange_cfg.is_seq_print) {
@@ -5238,6 +5270,8 @@ int CLI::run(int argc, char **argv)
arrange_cfg.clearance_height_to_rod = height_to_rod;
arrange_cfg.clearance_height_to_lid = height_to_lid;
arrange_cfg.clearance_radius = clearance_radius;
arrange_cfg.nozzle_height = nozzle_height;
arrange_cfg.align_center = align_center;
arrange_cfg.printable_height = print_height;
arrange_cfg.min_obj_distance = 0;
if (arrange_cfg.is_seq_print) {
@@ -5529,6 +5563,26 @@ int CLI::run(int argc, char **argv)
}
finished_arrange = true;
}
// CLI has no m_plater, so PartPlateList::create_plate() never backfills
// wipe_tower_x/y for plates created here during arrange overflow (unlike GUI's
// set_default_wipe_tower_pos_for_plate()). Keep both arrays sized to the actual
// plate count so a later per-plate get_at() never silently reuses another plate's
// wipe tower position via ConfigOptionVector's out-of-range clamp.
{
int final_plate_count = partplate_list.get_plate_count();
ConfigOptionFloats* wipe_x_opt = m_print_config.option<ConfigOptionFloats>("wipe_tower_x");
ConfigOptionFloats* wipe_y_opt = m_print_config.option<ConfigOptionFloats>("wipe_tower_y");
if (wipe_x_opt && !wipe_x_opt->values.empty() && wipe_x_opt->values.size() < static_cast<size_t>(final_plate_count)) {
BOOST_LOG_TRIVIAL(info) << boost::format("wipe_tower_x had %1% entries for %2% plates, backfilling with entry 0")
% wipe_x_opt->values.size() % final_plate_count;
wipe_x_opt->values.resize(final_plate_count, wipe_x_opt->values.front());
}
if (wipe_y_opt && !wipe_y_opt->values.empty() && wipe_y_opt->values.size() < static_cast<size_t>(final_plate_count)) {
BOOST_LOG_TRIVIAL(info) << boost::format("wipe_tower_y had %1% entries for %2% plates, backfilling with entry 0")
% wipe_y_opt->values.size() % final_plate_count;
wipe_y_opt->values.resize(final_plate_count, wipe_y_opt->values.front());
}
}
original_model.clear_objects();
original_model.clear_materials();
}
@@ -6516,10 +6570,20 @@ int CLI::run(int argc, char **argv)
plate_data->nozzle_diameters = nozzle_diameter_str;
for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) {
// ConfigOptionVector::get_at() falls back to values.front() when the index is out of
// range, but that is undefined behavior when values is empty outright (e.g. filament_ids
// is never populated on a from-scratch slice with no --load-filaments) - guard every
// get_at() here on the vector actually having an entry at it->id before calling it.
bool valid_id = it->id >= 0;
std::string display_filament_type;
it->type = m_print_config.get_filament_type(display_filament_type, it->id);
it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF";
it->filament_id = filament_id?filament_id->get_at(it->id):"";
if (valid_id && filament_types && static_cast<size_t>(it->id) < filament_types->values.size())
it->type = m_print_config.get_filament_type(display_filament_type, it->id);
it->color = (valid_id && filament_color && static_cast<size_t>(it->id) < filament_color->values.size()) ?
filament_color->get_at(it->id) :
"#FFFFFF";
it->filament_id = (valid_id && filament_id && static_cast<size_t>(it->id) < filament_id->values.size()) ?
filament_id->get_at(it->id) :
"";
}
if (!plate_data->plate_thumbnail.is_valid()) {
@@ -7311,6 +7375,10 @@ bool CLI::setup(int argc, char **argv)
m_config.option(optdef.first, true);
set_data_dir(m_config.opt_string("datadir"));
if (!data_dir().empty() && !boost::filesystem::exists(data_dir())) {
boost::nowide::cerr << "Could not create data directory: " << data_dir() << std::endl;
return false;
}
//FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet.
if (!validity.empty()) {
@@ -7384,7 +7452,7 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn
<< std::endl
<< "Print setting priorities:" << std::endl
<< "\t1) setting values from the command line (highest priority)"<< std::endl
<< "\t2) setting values loaded with --load_settings and --load_filaments" << std::endl
<< "\t2) setting values loaded with --load-settings and --load-filaments" << std::endl
<< "\t3) setting values loaded from 3mf(lowest priority)" << std::endl;
/*if (include_print_options) {
@@ -7423,6 +7491,10 @@ bool CLI::export_models(IO::ExportFormat format, std::string path_dir)
for (ModelObject* model_object : model.objects)
{
const std::string path = this->output_filepath(*model_object, index++, format, path_dir);
if (path.empty()) {
boost::nowide::cerr << "Could not create output directory for STL export" << std::endl;
return false;
}
success = Slic3r::store_stl(path.c_str(), model_object, true);
if (success)
BOOST_LOG_TRIVIAL(info) << "Model successfully exported to " << path << std::endl;
@@ -7548,8 +7620,19 @@ std::string CLI::output_filepath(const ModelObject &object, unsigned int index,
output_path = subdir + "/"+file_name;
boost::filesystem::path subdir_path(subdir);
if (!boost::filesystem::exists(subdir_path))
boost::filesystem::create_directory(subdir_path);
if (!boost::filesystem::exists(subdir_path)) {
try {
boost::filesystem::create_directories(subdir_path);
} catch (const boost::filesystem::filesystem_error &ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to create output directory " << subdir_path.string() << ": " << ex.what();
}
if (!boost::filesystem::exists(subdir_path)) {
// Directory creation failed and won't succeed on a retry (same path, same cause) -
// signal failure now instead of letting every object in the model repeat the same
// doomed attempt and fail with a less specific "export failed" error later.
return std::string();
}
}
return output_path;
}

View File

@@ -229,7 +229,7 @@ public:
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
size_t idx() const { return m_idx; }
const BoundingBox& bbox() const { return m_bbox; }
Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); }
Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; }
private:
size_t m_idx;
BoundingBox m_bbox;

View File

@@ -1812,17 +1812,31 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option
// to the end of the value.
if (opt_base->type() == coBools && value.empty())
static_cast<ConfigOptionBools*>(opt_base)->values.push_back(!no);
else
else {
// Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way
// they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape
// will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be
// unescaped by the calling shell.
opt_vector->deserialize(value, true);
bool deserialized = false;
try {
deserialized = opt_vector->deserialize(value, true);
} catch (const std::exception &ex) {
// e.g. "nil" deserialized into a non-nullable vector option throws instead of
// returning false - treat that the same as any other invalid value here.
deserialized = false;
}
if (! deserialized) {
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
return false;
}
}
} else if (opt_base->type() == coBool) {
if (value.empty())
static_cast<ConfigOptionBool*>(opt_base)->value = !no;
else
opt_base->deserialize(value);
else if (! opt_base->deserialize(value)) {
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
return false;
}
} else if (opt_base->type() == coString) {
// Do not unescape single string values, the unescaping is left to the calling shell.
static_cast<ConfigOptionString*>(opt_base)->value = value;

View File

@@ -55,9 +55,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
boost::filesystem::path temp_mtl_path(mtl_file);
mtl_path = temp_mtl_path;
}
auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str();
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
if (!ObjParser::mtlparse(_mtl_path, mtl_data)) {
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
message = _L("load mtl in obj: failed to parse");
return false;

View File

@@ -111,14 +111,19 @@ bool StepPreProcessor::isUtf8File(const char* path)
bool StepPreProcessor::isUtf8(const std::string str)
{
size_t num = 0;
int i = 0;
size_t i = 0;
while (i < str.length()) {
if ((str[i] & 0x80) == 0x00) {
const unsigned char lead = static_cast<unsigned char>(str[i]);
if ((lead & 0x80) == 0x00) {
i++;
} else if ((num = preNum(str[i])) > 2) {
// preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4
// bytes long, so anything outside that range is not a lead byte.
} else if ((num = preNum(lead)) >= 2 && num <= 4) {
if (i + num > str.length())
return false;
i++;
for (int j = 0; j < num - 1; j++) {
if ((str[i] & 0xc0) != 0x80)
for (size_t j = 0; j < num - 1; j++) {
if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80)
return false;
i++;
}
@@ -132,15 +137,20 @@ bool StepPreProcessor::isUtf8(const std::string str)
bool StepPreProcessor::isGBK(const std::string str) {
size_t i = 0;
while (i < str.length()) {
if (str[i] <= 0x7f) {
// char is signed here, so every byte compares <= 0x7f unless widened first.
const unsigned char lead = static_cast<unsigned char>(str[i]);
if (lead <= 0x7f) {
i++;
continue;
} else {
if (str[i] >= 0x81 &&
str[i] <= 0xfe &&
str[i + 1] >= 0x40 &&
str[i + 1] <= 0xfe &&
str[i + 1] != 0xf7) {
if (i + 1 >= str.length())
return false;
const unsigned char trail = static_cast<unsigned char>(str[i + 1]);
if (lead >= 0x81 &&
lead <= 0xfe &&
trail >= 0x40 &&
trail <= 0xfe &&
trail != 0xf7) {
i += 2;
continue;
}

View File

@@ -11948,13 +11948,11 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def = this->add("load_defaultfila", coBool);
def->label = L("Load default filaments");
def->tooltip = L("Load first filament as default for those not loaded.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("min_save", coBool);
def->label = L("Minimum save");
def->tooltip = L("Export 3MF with minimum size.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("mtcpp", coInt);
@@ -11980,7 +11978,6 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def = this->add("normative_check", coBool);
def->label = L("Normative check");
def->tooltip = L("Check the normative items.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(true));
/*def = this->add("help_fff", coBool);
@@ -12247,7 +12244,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def->cli_params = "level";
def->set_default_value(new ConfigOptionInt(1));
def = this->add("logfile", coInt);
def = this->add("logfile", coString);
def->label = L("Log file");
def->tooltip = L("Redirects debug logging to file.\n");
def->cli_params = "file";
@@ -12295,7 +12292,6 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("skip_modified_gcodes", coBool);
def->label = L("Skip modified G-code in 3MF");
def->tooltip = L("Skip the modified G-code in 3MF from printer or filament presets.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("makerlab_name", coString);
@@ -12325,14 +12321,12 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("allow_newer_file", coBool);
def->label = L("Allow 3MF with newer version to be sliced");
def->tooltip = L("Allow 3MF with newer version to be sliced.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("allow_mix_temp", coBool);
// internal use only, don't need translation
def->label = "Allow filaments with high/low temperature to be printed together";
def->tooltip = "Allow filaments with high/low temperature to be printed together.";
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
}

View File

@@ -310,7 +310,11 @@ void set_data_dir(const std::string &dir)
{
g_data_dir = dir;
if (!g_data_dir.empty() && !boost::filesystem::exists(g_data_dir)) {
boost::filesystem::create_directory(g_data_dir);
try {
boost::filesystem::create_directories(g_data_dir);
} catch (const boost::filesystem::filesystem_error &ex) {
BOOST_LOG_TRIVIAL(error) << "set_data_dir: failed to create data directory " << g_data_dir << ": " << ex.what();
}
}
}

View File

@@ -1757,26 +1757,25 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
else
obj_support = glb_support;
if (!obj_support)
continue;
if (obj_support) {
int obj_support_intf_extr = 0;
const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament");
if (support_intf_extr_opt != nullptr)
obj_support_intf_extr = support_intf_extr_opt->getInt();
if (obj_support_intf_extr != 0)
plate_extruders.push_back(obj_support_intf_extr);
else if (glb_support_intf_extr != 0)
plate_extruders.push_back(glb_support_intf_extr);
int obj_support_intf_extr = 0;
const ConfigOption* support_intf_extr_opt = object->config.option("support_interface_filament");
if (support_intf_extr_opt != nullptr)
obj_support_intf_extr = support_intf_extr_opt->getInt();
if (obj_support_intf_extr != 0)
plate_extruders.push_back(obj_support_intf_extr);
else if (glb_support_intf_extr != 0)
plate_extruders.push_back(glb_support_intf_extr);
int obj_support_extr = 0;
const ConfigOption* support_extr_opt = object->config.option("support_filament");
if (support_extr_opt != nullptr)
obj_support_extr = support_extr_opt->getInt();
if (obj_support_extr != 0)
plate_extruders.push_back(obj_support_extr);
else if (glb_support_extr != 0)
plate_extruders.push_back(glb_support_extr);
int obj_support_extr = 0;
const ConfigOption* support_extr_opt = object->config.option("support_filament");
if (support_extr_opt != nullptr)
obj_support_extr = support_extr_opt->getInt();
if (obj_support_extr != 0)
plate_extruders.push_back(obj_support_extr);
else if (glb_support_extr != 0)
plate_extruders.push_back(glb_support_extr);
}
int obj_outer_wall_extr = 0;
if (const ConfigOption* wall_opt = object->config.option("outer_wall_filament_id"); wall_opt != nullptr)
@@ -2771,19 +2770,16 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking
if (instance_box.min.z() < SINKING_Z_THRESHOLD) {
// Orca: For sinking object, we use a more expensive algorithm so part below build plate won't be considered
// m_plater is null in CLI mode.
if (m_plater && plate_box.intersects(instance_box)) {
// TODO: FIXME: this does not take exclusion area into account
const BuildVolume build_volume(get_shape(), m_plater->build_volume().printable_height(), m_extruder_areas, m_extruder_heights);
const auto state = instance->calc_print_volume_state(build_volume);
outside = state == ModelInstancePVS_Partly_Outside;
}
}
else
if (plate_box.contains(instance_box))
{
if (m_exclude_bounding_box.size() > 0)
// For sinking object, we use a more expensive algorithm so part below build plate won't be considered.
// m_height mirrors the printer's printable height independent of m_plater, so this runs in CLI too.
if (plate_box.intersects(instance_box)) {
// TODO: FIXME: this does not take exclusion area into account
const BuildVolume build_volume(get_shape(), m_height, m_extruder_areas, m_extruder_heights);
const auto state = instance->calc_print_volume_state(build_volume);
outside = state == ModelInstancePVS_Partly_Outside;
}
} else if (plate_box.contains(instance_box)) {
if (m_exclude_bounding_box.size() > 0)
{
Polygon hull = instance->convex_hull_2d();
int index;
@@ -2801,9 +2797,9 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
}
else
outside = false;
}
}
return outside;
return outside;
}
//judge whether instance is intesected with plate or not
@@ -6470,7 +6466,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence;
plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence;
plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment;
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time);
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->m_gcode_result->initial_layer_time);
Print *print = nullptr;
m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr);
if (print) {

View File

@@ -681,7 +681,7 @@ SearchDialog::SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindo
SearchDialog::~SearchDialog() {}
void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
void SearchDialog::Popup(wxWindow *focus /*= nullptr*/)
{
/* const std::string& line = searcher->search_string();
search_line->SetValue(line.empty() ? default_string : from_u8(line));
@@ -696,17 +696,19 @@ void SearchDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
search_line2->SetValue(wxString(""));
//const std::string &line = searcher->search_string();
//searcher->search(into_u8(line), true);
PopupWindow::Popup();
PopupWindow::Popup(focus);
search_line2->SetFocus();
update_list();
}
#ifdef __WXMSW__
void SearchDialog::MSWDismissUnfocusedPopup()
{
Dismiss();
OnDismiss();
}
#endif // __WXMSW__
void SearchDialog::OnDismiss() { }
@@ -926,7 +928,7 @@ SearchObjectDialog::SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* p
SearchObjectDialog::~SearchObjectDialog() {}
void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
void SearchObjectDialog::Popup(wxWindow *focus /*= nullptr*/)
{
if (m_is_dismissing || this->IsShown()) {
return;
@@ -937,7 +939,7 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
// dropdown list, otherwise the text input won't be usable
m_object_list->SetFocus();
#endif
PopupWindow::Popup();
PopupWindow::Popup(focus);
search_line2->SetFocus();
m_object_list->assembly_plate_object_name();
@@ -945,11 +947,13 @@ void SearchObjectDialog::Popup(wxPoint position /*= wxDefaultPosition*/)
update_list();
}
#ifdef __WXMSW__
void SearchObjectDialog::MSWDismissUnfocusedPopup()
{
Dismiss();
OnDismiss();
}
#endif // __WXMSW__
void SearchObjectDialog::OnDismiss() {}

View File

@@ -216,10 +216,12 @@ public:
SearchDialog(OptionsSearcher *searcher, Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *search_btn);
~SearchDialog();
void MSWDismissUnfocusedPopup();
void Popup(wxPoint position = wxDefaultPosition);
void OnDismiss();
void Dismiss();
#ifdef __WXMSW__
void MSWDismissUnfocusedPopup() override;
#endif // __WXMSW__
void Popup(wxWindow *focus = nullptr) override;
void OnDismiss() override;
void Dismiss() override;
void Die();
void msw_rescale();
@@ -260,10 +262,12 @@ public:
SearchObjectDialog(GUI::ObjectList* object_list, wxWindow* parent, TextInput* input);
~SearchObjectDialog();
void MSWDismissUnfocusedPopup();
void Popup(wxPoint position = wxDefaultPosition);
void OnDismiss();
void Dismiss();
#ifdef __WXMSW__
void MSWDismissUnfocusedPopup() override;
#endif // __WXMSW__
void Popup(wxWindow *focus = nullptr) override;
void OnDismiss() override;
void Dismiss() override;
void Die();
void OnInputText(wxCommandEvent& event);

View File

@@ -98,7 +98,7 @@ void LabeledStaticBox::SetBorderColor(StateColor const &color)
Refresh();
}
void LabeledStaticBox::SetFont(wxFont set_font)
bool LabeledStaticBox::SetFont(const wxFont &set_font)
{
m_font = set_font;
@@ -109,6 +109,7 @@ void LabeledStaticBox::SetFont(wxFont set_font)
m_label_width = tW;
Refresh();
return true;
}
bool LabeledStaticBox::Enable(bool enable)

View File

@@ -42,7 +42,7 @@ public:
void SetBorderColor(StateColor const &color);
void SetFont(wxFont set_font);
bool SetFont(const wxFont &set_font) override;
bool Enable(bool enable) override;

View File

@@ -21,6 +21,8 @@ ScrolledWindow::ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position
m_bottomScrollbar = NULL;
m_verticalSplitter = NULL;
m_horizontalSplitter = NULL;
m_userPanel = NULL;
m_scroll_win = NULL;
m_marginWidth = marginWidth;
@@ -110,12 +112,13 @@ void ScrolledWindow::SetTipColor(wxColour color)
if (m_bottomScrollbar) m_bottomScrollbar->SetTipColor(color);
}
void ScrolledWindow::SetBackgroundColour(wxColour color)
bool ScrolledWindow::SetBackgroundColour(const wxColour &color)
{
wxWindow::SetBackgroundColour(color);
const bool result = wxWindow::SetBackgroundColour(color);
m_verticalSplitter->SetBackgroundColour(color);
m_userPanel->SetBackgroundColour(color);
m_scroll_win->SetBackgroundColour(color);
return result;
}
void ScrolledWindow::SetMarginColor(wxColour color)

View File

@@ -15,7 +15,7 @@ public:
ScrolledWindow(wxWindow *parent, wxWindowID id, wxPoint position, wxSize size, long style, int marginWidth = 0, int scrollbarWidth = 4, int tipLength = 0);
void OnMouseWheel(wxMouseEvent &event);
void SetTipColor(wxColour color);
void SetBackgroundColour(wxColour color);
bool SetBackgroundColour(const wxColour &color) override;
void SetMarginColor(wxColour color);
void SetScrollbarColor(wxColour color);
@@ -26,7 +26,7 @@ public:
// wxSplitterWindow* GetVerticalSplitter() { return m_verticalSplitter; }
// wxSplitterWindow* GetHorizontalSplitter() { return m_horizontalSplitter; }
bool IsBothDirections() { return m_bothDirections; }
virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false);
virtual void SetScrollbars(int pixelsPerUnitX, int pixelsPerUnitY, int noUnitsX, int noUnitsY, int xPos = 0, int yPos = 0, bool noRefresh = false) override;
private:
wxPanel * m_userPanel; // the panel targeted by the scrolled window

View File

@@ -631,7 +631,7 @@ void parse_metadata_rfc822(const std::string& content,
bool is_ignored_plugin_directory(const boost::filesystem::path& path)
{
const std::string name = path.filename().string();
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR || name == PLUGIN_DATA_DIR;
}
bool is_safe_relative_path(const boost::filesystem::path& path)

View File

@@ -12,6 +12,7 @@
#include <vector>
#define PLUGIN_SUBSCRIBED_DIR "_subscribed"
#define PLUGIN_DATA_DIR "plugin_data"
namespace Slic3r {

View File

@@ -522,6 +522,38 @@ bool PluginManager::try_get_plugin_descriptor_for_capability(const std::string&
return false;
}
std::string PluginManager::get_storage_dir(const std::string& plugin_key) const
{
namespace fs = boost::filesystem;
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor))
throw std::runtime_error("The current plugin is not registered");
const fs::path base_storage_dir = fs::path(get_orca_plugins_dir()) / PLUGIN_DATA_DIR;
if (!descriptor.is_cloud_plugin()) {
const fs::path local_storage_dir = base_storage_dir / plugin_key;
fs::create_directories(local_storage_dir);
return local_storage_dir.string();
}
auto agent = m_cloud_service.get_cloud_agent();
if (!agent)
throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized");
const std::string user_id = agent->get_user_id();
if (user_id.empty())
throw std::runtime_error("Cloud plugin storage is unavailable without a logged-in user");
if (!is_valid_plugin_id(plugin_key))
throw std::runtime_error("The current cloud plugin key is not a valid folder name");
const fs::path cloud_storage_dir = base_storage_dir / PLUGIN_SUBSCRIBED_DIR / user_id / plugin_key;
fs::create_directories(cloud_storage_dir);
return cloud_storage_dir.string();
}
// ── Capability instances ────────────────────────────────────────────────────────────────────
std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugin_capabilities(const std::string& plugin_key,

View File

@@ -143,6 +143,10 @@ public:
bool try_get_plugin_descriptor_for_capability(const std::string& capability_name,
PluginCapabilityType type,
PluginDescriptor& out) const;
// Per-plugin storage directory under orca_plugins/plugin_data, created if missing. Throws
// std::runtime_error if the plugin is unregistered, the key is invalid, or (cloud plugins)
// no user is logged in yet.
std::string get_storage_dir(const std::string& plugin_key) const;
std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities(
const std::string& plugin_key = "", // "" => all plugins

View File

@@ -1,9 +1,31 @@
#include "PluginHost.hpp"
#include "PluginHostBindings.hpp"
#include "PluginHostUi.hpp"
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <stdexcept>
namespace Slic3r {
namespace host_bindings {
void register_plugin(pybind11::module_& host)
{
auto plugin_host = host.def_submodule("plugin", "Plugin host API");
plugin_host.def(
"storage",
[]() -> std::string {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty())
throw std::runtime_error("plugin.storage() must be called from a plugin callback");
return PluginManager::instance().get_storage_dir(plugin_key);
},
"Return the installed folder of the current plugin.");
}
} // namespace host_bindings
void PluginHost::RegisterBindings(pybind11::module_& module)
{
auto host = module.def_submodule("host", "Host application API");
@@ -15,6 +37,7 @@ void PluginHost::RegisterBindings(pybind11::module_& module)
host_bindings::register_presets(host);
host_bindings::register_model(host);
host_bindings::register_app(host);
host_bindings::register_plugin(host);
// UI: native dialogs and interactive HTML windows for plugins.
PluginHostUi::RegisterBindings(host);

View File

@@ -12,5 +12,5 @@ void register_presets(pybind11::module_& host); // PluginHostPresets.cpp
void register_model(pybind11::module_& host); // PluginHostModel.cpp
void register_app(pybind11::module_& host); // PluginHostApp.cpp
void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp
void register_plugin(pybind11::module_& host); // PluginHost.cpp
} // namespace Slic3r::host_bindings

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_nozzle_volume_type.cpp
test_step.cpp
test_stl.cpp
test_triangle_selector.cpp
test_meshboolean.cpp

View File

@@ -0,0 +1,93 @@
#include <catch2/catch_all.hpp>
#include <boost/nowide/fstream.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/STEP.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
static void write_step_line(const std::string &path, const std::string &line)
{
boost::nowide::ofstream file(path, std::ios::binary);
file << "ISO-10303-21;\n" << line << "\nEND-ISO-10303-21;\n";
}
// preprocess() hands back the input path unless it transcoded into a temporary.
static std::string preprocess_result(const std::string &line)
{
ScopedSlic3rTemporaryDir scratch;
ScopedTemporaryFile step(".step");
write_step_line(step.string(), line);
std::string output_path;
StepPreProcessor preprocessor;
REQUIRE(preprocessor.preprocess(step.string().c_str(), output_path));
return output_path == step.string() ? "untouched" : "transcoded";
}
// data/utf8_part_names.step is three boxes written by OCCT's own STEP writer, whose
// PRODUCT names were then patched to raw UTF-8. Most CAD exporters write non-ASCII names
// that way rather than in the \X2\ escape form. The third part is ASCII, as a control.
TEST_CASE("Part names with multi-byte UTF-8 survive import", "[Step]")
{
// getNamedSolids() replaces a name that isUtf8() rejects with a running number.
const std::string path = TEST_DATA_DIR PATH_SEPARATOR "utf8_part_names.step";
Model model;
bool cancel = false;
Step step(path); // no isUtf8Fn, matching how Model::read_from_step builds it
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
REQUIRE(model.objects.size() == 1);
const ModelObject *object = model.objects.front();
REQUIRE(object->volumes.size() == 3);
// "ce" is split off, or the hex escape would swallow it as further hex digits.
CHECK(object->volumes[0]->name == "pi\xC3\xA8" "ce");
CHECK(object->volumes[1]->name == "Geh\xC3\xA4use");
CHECK(object->volumes[2]->name == "bracket");
}
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
{
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9
CHECK(StepPreProcessor::isUtf8("\xE4\xB8\xAD")); // U+4E2D
CHECK(StepPreProcessor::isUtf8("\xF0\x9F\x94\xA9")); // U+1F529
CHECK_FALSE(StepPreProcessor::isUtf8("\x81\x30")); // 0x81 is not a lead byte
CHECK_FALSE(StepPreProcessor::isUtf8("\xC3")); // truncated sequence
}
// The only caller of isGBK is preprocess(), which nothing calls today.
TEST_CASE("Encoding detection decides whether a step file is transcoded", "[Step]")
{
SECTION("UTF-8, so left alone")
{
// A two byte sequence also satisfies every GBK range, so misdetecting it as
// not-UTF-8 sends it to be transcoded.
const std::string sequence = GENERATE(std::string("\xC3\xA9"), // U+00E9
std::string("\xE4\xB8\xAD"), // U+4E2D
std::string("\xF0\x9F\x94\xA9")); // U+1F529
CHECK(preprocess_result("NAME('" + sequence + "');") == "untouched");
}
SECTION("neither UTF-8 nor GBK, so left alone")
{
// 0x81 is not a UTF-8 lead byte, and 0x30 is below the 0x40 floor for a GBK trail.
CHECK(preprocess_result("NAME('\x81\x30');") == "untouched");
}
SECTION("GBK, so transcoded")
{
// U+554A in GBK, whose lead byte is not valid UTF-8. Pins the other direction,
// since a detector that never reports GBK would pass every case above.
CHECK(preprocess_result("NAME('\xB0\xA1');") == "transcoded");
}
SECTION("plain ASCII, so left alone") { CHECK(preprocess_result("NAME('bracket');") == "untouched"); }
}

View File

@@ -4,6 +4,7 @@
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/Format/OBJ.hpp>
#include <libslic3r/SVG.hpp>
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
@@ -32,7 +33,7 @@ inline Slic3r::TriangleMesh load_model(const std::string &obj_filename)
// ---------------------------------------------------------------------------
// Owns a unique path under the system temp dir, "<prefix>-<unique>[<extension>]"
// (parallel-safe, cross-platform). Shared base for the two RAII temp guards below.
// (parallel-safe, cross-platform). Shared base for the RAII temp guards below.
class ScopedTemporaryPath
{
public:
@@ -70,6 +71,24 @@ public:
~ScopedTemporaryDir() { boost::system::error_code ec; boost::filesystem::remove_all(m_path, ec); }
};
// A temp directory that is also Slic3r::temporary_dir() for its lifetime. No test
// process sets that global, so code under test which writes there (for example
// StepPreProcessor::preprocess) lands at the filesystem root. Restored on scope exit
// even when an assertion throws, so it cannot leak into later tests.
class ScopedSlic3rTemporaryDir : public ScopedTemporaryDir
{
public:
explicit ScopedSlic3rTemporaryDir(const std::string &prefix = "orca")
: ScopedTemporaryDir(prefix), m_previous(Slic3r::temporary_dir())
{ Slic3r::set_temporary_dir(string()); }
// Runs before ~ScopedTemporaryDir, so the setting goes back while the directory
// it names still exists.
~ScopedSlic3rTemporaryDir() { Slic3r::set_temporary_dir(m_previous); }
private:
const std::string m_previous;
};
// ---------------------------------------------------------------------------
// Debug-only test artifacts
//