Compare commits

...

7 Commits

Author SHA1 Message Date
Kris Austin
efc9f253ee fix: resolve relative input paths given on the command line (#14803)
Opening a model with a relative path, for example `orca-slicer ./some.3mf`,
failed with "Loading of a model file failed." and "The file does not contain
any geometry data.", while the same file opened by an absolute path or by
drag and drop worked.

GUI_App::init_app_config() changes the working directory to <data_dir>/log,
and it runs from the GUI_App constructor because the app config is needed
early for instance checking. The input files are opened much later, in
post_init(), so a path still relative at that point resolved against the log
directory instead of the directory OrcaSlicer was started from, and the 3MF
reader failed to open it.

Resolve the input paths in CLI::setup(), which runs before GUI_App is
constructed and therefore before the working directory moves. Absolute paths
are returned unchanged, so the forms that open today are unaffected, and
custom open protocol URLs are passed through since post_init() hands those to
the downloader rather than the file loader.

The working directory change is left alone. It was added in #3248 so the TUTK
logs land in the data directory instead of the working directory (#3209).
2026-09-15 12:47:01 +08:00
Kris Austin
292cf0095e drop the per-frame mouse raycast that only a drag start reads (#15664) 2026-09-14 18:31:23 -03:00
Kris Austin
5c635d5e50 build: scope -Werror to the Clang family so GCC builds again (#15701) 2026-09-14 17:04:03 -03:00
Nopraz
5496883493 fix(profiles): Snapmaker U1 — cap ABS/ASA/PPS bed temps at 100 °C (#15483)
The U1's heated bed tops out at 100 °C, but these profiles requested
105-110 °C, which leads to print errors unless the user modifies the
printer's firmware configuration.

Affected profiles:
- Snapmaker ABS @U1 base (110/105 → 100)
- Snapmaker ASA @U1 base (110 → 100)
- Fiberon ASA-CF08 @Snapmaker U1 base (105 → 100)
- Fiberon PPS-GF20 @Snapmaker U1 base (105 → 100)

Bumps Snapmaker.json to 02.04.00.10.

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-14 20:44:15 +03:00
Daniel Williams
70247ad298 Extract Layer::choose_ironing_extruder for unit-testable ironing routing (#13467)
* Extract Layer::choose_ironing_extruder for unit-testable ironing routing

The ironing extruder selection in make_ironing() was a 5-line nested
conditional inlined at the top of the loop, with no isolated test
coverage. Pull the gating into a static helper so the routing decision
is unit-testable without spinning up the slicing pipeline.

Pure refactor: the helper preserves the original logic bit-for-bit
(NoIroning -> -1; AllSolid always enabled; TopSurfaces and TopmostOnly
require some top shells or, in spiral mode, more than one bottom shell;
TopmostOnly additionally requires being on the topmost layer; enabled
ironing routes to solid_infill_filament).

Add tests/fff_print/test_choose_ironing_extruder.cpp covering:
- AllSolid regardless of layer position
- TopSurfaces with top_shell_layers > 0
- TopSurfaces with top_shell_layers=0 + spiral mode + bottom_shell_layers>1
- TopmostOnly + topmost layer
- NoIroning short-circuit
- TopSurfaces with top_shell_layers=0 (and not spiral) -> disabled
- TopSurfaces, spiral, but bottom_shell_layers=1 -> disabled
- TopmostOnly on a non-topmost layer -> disabled

* Move ironing routing test into the Fill subsystem file

Rename the test to tests/libslic3r/test_fill.cpp and tag it [Fill] to
match the subsystem it covers, use flat behavioral test cases with
GENERATE for the parameterized ones, and drop the history narration from
the code comments.

* tests: move ironing routing tests into fff_print/test_fill.cpp

Keeps the Fill tests in one file, alongside the existing ironing
rotation-template test.
2026-09-14 09:37:04 -03:00
Lam Wei Lun
ecbe1b1b90 UI Bug fixes and code cleanup for Publish 3MF Dialog (#15690)
# Description
- Fixes an issue on macOS where the modified indicator can be cut-off.
- Remove unused code
2026-09-14 16:57:41 +08:00
Lam Wei Lun
ffb4f192c1 Fix macOS UI issue in publish dialog. Remove item_size helper in TabCtrl and its relevant setter 2026-09-14 14:19:25 +08:00
20 changed files with 244 additions and 84 deletions

View File

@@ -587,10 +587,15 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
add_compile_options(-Wno-${w}) add_compile_options(-Wno-${w})
endforeach () endforeach ()
# Turn everything else into an error. Dependency headers are exempt because the SYSTEM # GCC is not built in CI, so don't throw errors CI won't catch.
# include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their diagnostics out, if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# apart from GCC's maybe-uninitialized, demoted below. add_compile_options(-Werror=return-type)
else ()
# Turn everything else into an error. Dependency headers are exempt because the
# SYSTEM include flag (-imsvc on clang-cl, -isystem elsewhere) keeps their
# diagnostics out.
add_compile_options(-Werror) add_compile_options(-Werror)
endif ()
# Demoted. Remove a name once its category is cleared on every compiler. # Demoted. Remove a name once its category is cleared on every compiler.
set(warnings_demoted) set(warnings_demoted)
@@ -612,20 +617,6 @@ if ((NOT MSVC OR IS_CLANG_CL) AND ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
cast-function-type-mismatch cast-function-type-mismatch
) )
endif () endif ()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND warnings_demoted
# maybe-uninitialized runs after inlining and reports inside boost/variant,
# boost/tuple and the bundled clipper header even with -isystem.
maybe-uninitialized
# array-bounds is reported once, where ConfigOptionVector::set_at inlines
# into OrcaSlicer.cpp on a branch the preceding type test rules out.
array-bounds
# template-id-cdtor is a GCC 14+ warning in the bundled Clipper2 headers.
template-id-cdtor
)
endif ()
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND warnings_demoted list(APPEND warnings_demoted
# enum-constexpr-conversion is a Clang warning that defaults to an error, # enum-constexpr-conversion is a Clang warning that defaults to an error,

View File

@@ -1,6 +1,6 @@
{ {
"name": "Snapmaker", "name": "Snapmaker",
"version": "02.04.00.12", "version": "02.04.00.13",
"force_update": "0", "force_update": "0",
"description": "Snapmaker configurations", "description": "Snapmaker configurations",
"machine_model_list": [ "machine_model_list": [

View File

@@ -15,13 +15,13 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"105" "100"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"105" "100"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"105" "100"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"100" "100"
@@ -48,7 +48,7 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"105" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"
@@ -72,7 +72,7 @@
"110.8" "110.8"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"105" "100"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"100" "100"

View File

@@ -15,16 +15,16 @@
"1" "1"
], ],
"cool_plate_temp": [ "cool_plate_temp": [
"105" "100"
], ],
"cool_plate_temp_initial_layer": [ "cool_plate_temp_initial_layer": [
"105" "100"
], ],
"eng_plate_temp": [ "eng_plate_temp": [
"105" "100"
], ],
"eng_plate_temp_initial_layer": [ "eng_plate_temp_initial_layer": [
"105" "100"
], ],
"fan_cooling_layer_time": [ "fan_cooling_layer_time": [
"12" "12"
@@ -51,10 +51,10 @@
"Polymaker" "Polymaker"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"105" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"105" "100"
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"300" "300"
@@ -81,10 +81,10 @@
"110" "110"
], ],
"textured_plate_temp": [ "textured_plate_temp": [
"105" "100"
], ],
"textured_plate_temp_initial_layer": [ "textured_plate_temp_initial_layer": [
"105" "100"
], ],
"filament_type": [ "filament_type": [
"ABS" "ABS"

View File

@@ -9,10 +9,10 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"110" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"105" "100"
], ],
"overhang_fan_speed": [ "overhang_fan_speed": [
"20" "20"

View File

@@ -9,7 +9,7 @@
"" ""
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"110" "100"
], ],
"hot_plate_temp_initial_layer": [ "hot_plate_temp_initial_layer": [
"100" "100"

View File

@@ -7715,6 +7715,13 @@ bool CLI::setup(int argc, char **argv)
this->print_help(); this->print_help();
return false; return false;
} }
// Orca: resolve here, while the process is still in the directory the user invoked it from.
// GUI_App's constructor moves the working directory to <data_dir>/log, long before the GUI
// opens these files in post_init(), and a relative path would then resolve against that.
for (std::string &input_file : m_input_files)
input_file = resolve_cli_input_path(input_file);
// Parse actions and transform options. // Parse actions and transform options.
for (auto const &opt_key : opt_order) { for (auto const &opt_key : opt_order) {
if (cli_actions_config_def.has(opt_key)) if (cli_actions_config_def.has(opt_key))

View File

@@ -1595,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
return sparse_infill_polylines; return sparse_infill_polylines;
} }
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
// either some top shells or, in spiral mode, more than one bottom shell, and
// TopmostOnly additionally needs the layer to be the topmost one.
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer)
{
if (cfg.ironing_type == IroningType::NoIroning)
return -1;
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
&& (cfg.ironing_type == IroningType::TopSurfaces
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
if (!gate)
return -1;
return cfg.top_surface_filament_id;
}
// Create ironing extrusions over top surfaces. // Create ironing extrusions over top surfaces.
void Layer::make_ironing() void Layer::make_ironing()
{ {
@@ -1664,19 +1683,10 @@ void Layer::make_ironing()
if (! layerm->slices.empty()) { if (! layerm->slices.empty()) {
IroningParams ironing_params; IroningParams ironing_params;
const PrintRegionConfig &config = layerm->region().config(); const PrintRegionConfig &config = layerm->region().config();
if (config.ironing_type != IroningType::NoIroning && ironing_params.extruder = Layer::choose_ironing_extruder(
(config.ironing_type == IroningType::AllSolid || config,
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) && /*spiral_mode=*/this->object()->print()->config().spiral_mode,
(config.ironing_type == IroningType::TopSurfaces || /*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr);
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
// Iron the whole face.
ironing_params.extruder = config.top_surface_filament_id;
} else {
// Iron just the infill.
ironing_params.extruder = config.top_surface_filament_id;
}
}
if (ironing_params.extruder != -1) { if (ironing_params.extruder != -1) {
//TODO just_infill is currently not used. //TODO just_infill is currently not used.
ironing_params.just_infill = false; ironing_params.just_infill = false;

View File

@@ -16,6 +16,7 @@ using LayerPtrs = std::vector<Layer*>;
class LayerRegion; class LayerRegion;
using LayerRegionPtrs = std::vector<LayerRegion*>; using LayerRegionPtrs = std::vector<LayerRegion*>;
class PrintRegion; class PrintRegion;
class PrintRegionConfig;
class PrintObject; class PrintObject;
class Print; class Print;
@@ -200,6 +201,11 @@ public:
FillAdaptive::Octree *support_fill_octree, FillAdaptive::Octree *support_fill_octree,
FillLightning::Generator* lightning_generator) const; FillLightning::Generator* lightning_generator) const;
void make_ironing(); void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed.
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer);
void make_contour_z(const sla::IndexedMesh &mesh); void make_contour_z(const sla::IndexedMesh &mesh);
void export_region_slices_to_svg(const char *path) const; void export_region_slices_to_svg(const char *path) const;

View File

@@ -314,6 +314,9 @@ extern unsigned get_current_pid();
std::string per_user_temp_id(); std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged. // Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id); std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// Completes a relative command line input path against the current working directory. Absolute
// paths and custom open protocol URLs are returned unchanged.
std::string resolve_cli_input_path(const std::string &path);
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid); std::string get_process_name(int pid);

View File

@@ -1339,6 +1339,19 @@ std::string per_user_temp_dir(const std::string &base, const std::string &user_i
return base + "/orcaslicer_" + user_id; return base + "/orcaslicer_" + user_id;
} }
std::string resolve_cli_input_path(const std::string &path)
{
const boost::filesystem::path input(path);
if (path.empty() || is_supported_open_protocol(path) || input.is_absolute())
return path;
boost::system::error_code ec;
const boost::filesystem::path resolved = boost::filesystem::system_complete(input, ec);
if (ec)
return path;
return resolved.lexically_normal().make_preferred().string();
}
// BBS: backup & restore // BBS: backup & restore
std::string get_process_name(int pid) std::string get_process_name(int pid)
{ {

View File

@@ -2098,12 +2098,6 @@ void GLCanvas3D::render(bool only_init)
_render_selection_center(); _render_selection_center();
#endif // ENABLE_RENDER_SELECTION_CENTER #endif // ENABLE_RENDER_SELECTION_CENTER
// we need to set the mouse's scene position here because the depth buffer
// could be invalidated by the following gizmo render methods
// this position is used later into on_mouse() to drag the objects
if (m_picking_enabled)
m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
// sidebar hints need to be rendered before the gizmos because the depth buffer // sidebar hints need to be rendered before the gizmos because the depth buffer
// could be invalidated by the following gizmo render methods // could be invalidated by the following gizmo render methods
_render_selection_sidebar_hints(); _render_selection_sidebar_hints();
@@ -4491,12 +4485,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box(); BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
volume_bbox.offset(1.0); volume_bbox.offset(1.0);
const bool is_cut_connector_selected = m_selection.is_any_connector(); const bool is_cut_connector_selected = m_selection.is_any_connector();
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position) && !is_cut_connector_selected) { const Vec3d scene_position = _mouse_to_3d(pos);
if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(scene_position) && !is_cut_connector_selected) {
m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None; m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
// The dragging operation is initiated. // The dragging operation is initiated.
m_mouse.drag.move_volume_idx = volume_idx; m_mouse.drag.move_volume_idx = volume_idx;
m_selection.setup_cache(); m_selection.setup_cache();
m_mouse.drag.start_position_3D = m_mouse.scene_position; m_mouse.drag.start_position_3D = scene_position;
m_sequential_print_clearance_first_displacement = true; m_sequential_print_clearance_first_displacement = true;
m_moving = true; m_moving = true;

View File

@@ -337,7 +337,6 @@ class GLCanvas3D
bool dragging{ false }; bool dragging{ false };
Vec2d position{ DBL_MAX, DBL_MAX }; Vec2d position{ DBL_MAX, DBL_MAX };
Vec3d scene_position{ DBL_MAX, DBL_MAX, DBL_MAX };
bool ignore_left_up{ false }; bool ignore_left_up{ false };
Drag drag; Drag drag;
bool ignore_right_up; bool ignore_right_up;

View File

@@ -1037,9 +1037,6 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style);
section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetFont(Label::Body_14);
section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour());
// The mixed tabs carry full swatch compositions: give them a touch more room than the
// filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem).
section.mixed_tabs->SetItemSpace(FromDIP(3));
page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2));
section.mixed_tabs->Hide(); section.mixed_tabs->Hide();
} }

View File

@@ -311,8 +311,11 @@ void Button::render(wxDC& dc)
} }
} }
auto szContent = textSize; auto szContent = textSize;
// Whether the measured content reserved the text/icon gap. macOS measures an empty label
// as 0-high, so the gap is skipped there; the dot must not advance past it in that case.
const bool gap_reserved = szContent.y > 0;
if (icon.bmp().IsOk()) { if (icon.bmp().IsOk()) {
if (szContent.y > 0) { if (gap_reserved) {
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) if (vertical)
szContent.y += spacing; szContent.y += spacing;
@@ -357,10 +360,10 @@ void Button::render(wxDC& dc)
dc.DrawBitmap(icon.bmp(), pt); dc.DrawBitmap(icon.bmp(), pt);
//BBS norrow size between text and icon //BBS norrow size between text and icon
if (vertical) { if (vertical) {
pt.y += szIcon.y + spacing; pt.y += szIcon.y + (gap_reserved ? spacing : 0);
pt.x = rcContent.x; pt.x = rcContent.x;
} else { } else {
pt.x += szIcon.x + spacing; pt.x += szIcon.x + (gap_reserved ? spacing : 0);
pt.y = rcContent.y; pt.y = rcContent.y;
} }
} }

View File

@@ -99,7 +99,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli
btns.push_back(btn); btns.push_back(btn);
if (btns.size() > 1) if (btns.size() > 1)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space); sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL);
sizer->AddStretchSpacer(1); sizer->AddStretchSpacer(1);
relayout(); relayout();
return btns.size() - 1; return btns.size() - 1;
@@ -256,12 +256,12 @@ void TabCtrl::relayout()
int item = sel + 1; int item = sel + 1;
int first = 0; int first = 0;
for (int i = 0; i < item; ++i) for (int i = 0; i < item; ++i)
offset += btns[i]->GetMinSize().x + item_space * 2; offset += btns[i]->GetMinSize().x;
if (item < btns.size()) if (item < btns.size())
offset += btns[item]->GetMinSize().x + item_space * 2; offset += btns[item]->GetMinSize().x;
int width = GetSize().x; int width = GetSize().x;
for (int i = 0; i < btns.size(); ++i) { for (int i = 0; i < btns.size(); ++i) {
auto size = btns[i]->GetMinSize().x + item_space * 2; auto size = btns[i]->GetMinSize().x;
if (i < sel && offset > width) { if (i < sel && offset > width) {
sizer->Show(i * 2 + 1, false); sizer->Show(i * 2 + 1, false);
sizer->Show(i * 2 + 2, false); sizer->Show(i * 2 + 2, false);
@@ -284,26 +284,17 @@ void TabCtrl::relayout()
if (item >= btns.size()) if (item >= btns.size())
--item; --item;
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE // Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8; int b = GetSize().x - offset - 10 - (item + 1 - first) * 16;
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0}); sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
Layout(); Layout();
} }
void TabCtrl::SetItemSpace(int space)
{
if (space < 0 || space == item_space)
return;
item_space = space;
relayout();
Refresh();
}
int TabCtrl::GetFullSize() const int TabCtrl::GetFullSize() const
{ {
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing. // Mirrors relayout(): a 10px leading spacer plus every button's min width.
int width = 10; int width = 10;
for (const Button* btn : btns) for (const Button* btn : btns)
width += btn->GetMinSize().x + item_space * 2; width += btn->GetMinSize().x;
return width; return width;
} }

View File

@@ -14,7 +14,6 @@ class TabCtrl : public StaticBox
int sel = -1; int sel = -1;
wxFont bold; wxFont bold;
int item_space = 2; // space around each button, both sides (SetItemSpace)
public: public:
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0); TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
@@ -64,10 +63,6 @@ public:
int GetNextVisible(int item) const; int GetNextVisible(int item) const;
bool IsVisible(unsigned int item) const; bool IsVisible(unsigned int item) const;
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
// standard; call before appending items so every button picks it up.
void SetItemSpace(int space);
int GetFullSize() const; int GetFullSize() const;
private: private:

View File

@@ -15,6 +15,7 @@
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/SVG.hpp" #include "libslic3r/SVG.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
@@ -676,6 +677,73 @@ TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]")
REQUIRE(compared > int(ironing.size()) / 2); REQUIRE(compared > int(ironing.size()) / 2);
} }
namespace {
PrintRegionConfig ironing_config(IroningType type,
int top_surface_filament_id = 1,
int top_shell_layers = 3,
int bottom_shell_layers = 1)
{
PrintRegionConfig cfg;
cfg.ironing_type.value = type;
cfg.top_surface_filament_id.value = top_surface_filament_id;
cfg.top_shell_layers.value = top_shell_layers;
cfg.bottom_shell_layers.value = bottom_shell_layers;
cfg.outer_wall_filament_id.value = 1;
cfg.wall_loops.value = 2;
return cfg;
}
} // namespace
TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2);
const bool is_topmost_layer = GENERATE(false, true);
CAPTURE(is_topmost_layer);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2);
}
TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/3,
/*top_shell_layers=*/2);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3);
}
TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]")
{
const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/1,
/*top_shell_layers=*/0,
/*bottom_shell_layers=*/1);
const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces,
/*top_surface_filament_id=*/1,
/*top_shell_layers=*/0,
/*bottom_shell_layers=*/2);
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1);
REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1);
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
}
TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4);
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
}
TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
{
const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning);
const bool spiral_mode = GENERATE(false, true);
CAPTURE(spiral_mode);
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
}
TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]") TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]")
{ {
auto angles_for = [](int direction) { auto angles_for = [](int direction) {

View File

@@ -4,6 +4,8 @@
#include "test_utils.hpp" #include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <fstream> #include <fstream>
@@ -88,3 +90,65 @@ TEST_CASE("copy_file reports the OS error when the destination cannot be written
REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; })); REQUIRE(std::all_of(code.begin(), code.end(), [](unsigned char c) { return std::isdigit(c) != 0; }));
#endif // _WIN32 #endif // _WIN32
} }
TEST_CASE("A resolved input path still names the same file after the working directory changes", "[utils]") {
ScopedTemporaryFile model(".3mf");
{ std::ofstream out(model.string()); out << "3mf"; }
const std::string name = model.path().filename().string();
// Resolve the bare name from the directory holding the file, then move away from it. The guard
// restores the directory the test started in, wherever this leaves it.
ScopedWorkingDirectory cwd(model.path().parent_path());
const std::string resolved = resolve_cli_input_path(name);
boost::filesystem::current_path(boost::filesystem::path(TEST_DATA_DIR));
REQUIRE(boost::filesystem::exists(resolved));
REQUIRE(boost::filesystem::equivalent(resolved, model.path()));
// Control: the bare name finds nothing from here, so resolving it this late would have failed.
REQUIRE_FALSE(boost::filesystem::exists(name));
}
TEST_CASE("resolve_cli_input_path completes a relative path against the working directory", "[utils]") {
ScopedWorkingDirectory cwd(boost::filesystem::temp_directory_path());
// Read back rather than reusing temp_directory_path(): changing to it resolves any symlink.
const boost::filesystem::path here = boost::filesystem::current_path();
SECTION("a bare name") {
REQUIRE(resolve_cli_input_path("model.3mf") == (here / "model.3mf").make_preferred().string());
}
SECTION("a ./ prefix is dropped") {
REQUIRE(resolve_cli_input_path("./model.3mf") == (here / "model.3mf").make_preferred().string());
}
SECTION("a ../ traversal is collapsed") {
REQUIRE(resolve_cli_input_path("../model.3mf") == (here.parent_path() / "model.3mf").make_preferred().string());
}
}
TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed unchanged", "[utils]") {
SECTION("an absolute path") {
const boost::filesystem::path absolute = (boost::filesystem::temp_directory_path() / "model.3mf").make_preferred();
REQUIRE(resolve_cli_input_path(absolute.string()) == absolute.string());
}
#ifdef _WIN32
// Every absolute form Windows accepts opens today, so each must come back byte for byte:
// normalizing them would rewrite the forward slashes and rebuild the \\?\ and UNC prefixes.
SECTION("an absolute Windows path of any form") {
for (const std::string absolute : {R"(C:\models\model.3mf)",
R"(C:/models/model.3mf)",
R"(\\server\share\model.3mf)",
R"(\\?\C:\models\model.3mf)"})
REQUIRE(resolve_cli_input_path(absolute) == absolute);
}
#endif
// These are downloaded rather than opened, and completing one would produce a path, not a URL.
SECTION("a custom open protocol URL") {
for (const std::string url : {"orcaslicer://open/?file=https://example.com/model.3mf",
"prusaslicer://open/?file=https://example.com/model.3mf",
"bambustudio://open/?file=https://example.com/model.3mf",
"cura://open/?file=https://example.com/model.3mf"})
REQUIRE(resolve_cli_input_path(url) == url);
}
SECTION("an empty argument") {
REQUIRE(resolve_cli_input_path("").empty());
}
}

View File

@@ -176,4 +176,22 @@ inline void write_debug_stream([[maybe_unused]] const std::string &name, [[maybe
#endif #endif
} }
// Changes the working directory and restores the previous one on scope exit, including when an
// assertion throws. It is process wide state shared with every other test.
class ScopedWorkingDirectory
{
public:
explicit ScopedWorkingDirectory(const boost::filesystem::path &dir)
: m_previous(boost::filesystem::current_path())
{
boost::filesystem::current_path(dir);
}
~ScopedWorkingDirectory() { boost::system::error_code ec; boost::filesystem::current_path(m_previous, ec); }
ScopedWorkingDirectory(const ScopedWorkingDirectory &) = delete;
ScopedWorkingDirectory &operator=(const ScopedWorkingDirectory &) = delete;
private:
boost::filesystem::path m_previous;
};
#endif // SLIC3R_TEST_UTILS #endif // SLIC3R_TEST_UTILS