Compare commits

...
Author SHA1 Message Date
fd28f6c189 Fix Ctrl+A (select current plate) and Delete All skipping objects that no longer fit or while one of instances not on the bed (#15895)
* Fix Ctrl+A (select current plate) skipping objects that no longer fit the bed

Selection::add_curr_plate() used PartPlate::contain_instance_totally(),
which excludes any instance flagged in instance_outside_set -- an
instance too big for, or dragged outside, the bed. Since that is
exactly the kind of object a user reaches for Ctrl+A to move or
delete, it silently dropped out of "select all" (and out of the
selection entirely when it was the only object on the plate).

Selection::remove_curr_plate(), used for "delete all on this plate",
already does this correctly with the plain membership check,
PartPlate::contain_instance() (no outside-bed exclusion). Match it.

Likely reachable since #12236 changed Ctrl+A's target from "all
plates" to "current plate", switching it onto this pre-existing
function.

Fixes OrcaSlicer/OrcaSlicer#15888

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim comment verbosity per review feedback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix behaviour for instances

* simplify code

* Update Selection.cpp

* Update Selection.cpp

* update

* update

---------

Co-authored-by: Fernando Marino <f.marino@rheagroup.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-17 02:21:41 +03:00
Kris Austin 9b2fcaa53a fix: "bad allocation" when opening a 2.4 project with a custom filament (#16569) 2026-10-16 20:12:24 -03:00
Ian BassiandRodrigo Faselli 0acf1e6680 Fix wall order on narrow tube walls and under outer brims, and test wall order and direction (#16421)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-16 18:56:55 -03:00
Ján Mišenko c6804fbb68 fix: print statistics placeholders fail in custom G-code (#16348)
The G-code editor lists the Print Statistics placeholders for every
custom G-code field, but total_weight, used_filament, total_cost,
print_time, normal_print_time, silent_print_time,
total_wipe_tower_cost and total_wipe_tower_filament were only defined
for filename_format. Using one in custom G-code, including the File
header G-code, stopped slicing with "Not a variable name".

They are known only once the whole G-code is generated, so they use the
reserved-tag mechanism of print_time_sec and used_filament_length: the
placeholder parser gets a tag such as @TOTAL_WEIGHT@ and
GCodeProcessor::run_post_process() replaces it with the same value
filename_format gets, with numbers written to two decimals. The
per-tag replacement loops are folded into one helper. The running
extruded_* totals are unchanged.

Fixes #15320
Fixes #7870
2026-10-16 16:30:05 -05:00
Kiss Lorand 6d40ec93f6 Fix missing tree support paths and misplaced extra walls (#16549) 2026-10-16 18:04:57 -03:00
SoftFever 82aee1abdd Cache compiles in the macOS build script, including Xcode builds (#16553) 2026-10-16 19:36:50 +08:00
Ian Chua cefdd9b609 fix: max filepath length on windows (#16552)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Windows traditionally limits many file operations to `MAX_PATH` (260
characters). Deeply nested plugin packages can exceed this limit during
`.whl` extraction, leaving files missing and causing the plugin to fail
to load.

This change adds a shared extended_path helper that prepares paths for
Windows file operations using the extended-length prefixes `\\?\` or
`\\?\UNC\`. The helper is used for ZIP archive I/O and extraction
destinations, including plugin wheel extraction. This avoids relying on
the Windows long-path setting, which changes machine-wide settings.

Paths remain unprefixed outside file operations. Code that bypasses the
shared ZIP helpers may still require separate long-path handling.

See [Microsoft's documentation on path length
limitation](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry)

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
Tested on Windows 11 by using a portable installation of OrcaSlicer and
placing it in some really long filepath length e.g.
`C:/somename/<240 characters long folder path>/OrcaSlicer` and then
running and trying to install any plugin locally or on OrcaCloud.

<!--
> 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-10-16 18:47:49 +08:00
SoftFever caade80624 Keep the texture picker from stalling on open (#16547)
# Description

Opening the texture picker decoded every texture in the expanded
sections on the GUI thread in a single frame, each one twice at full
size. This stalled the UI on large libraries, and the full-size decodes
stayed in memory afterwards.

- Picker thumbnails are loaded lazily: only tiles inside the visible
scroll area are considered, at most 2 uncached decodes per frame, and
the rest fill in over the following frames. Deferred tiles draw as a
placeholder.
- Those follow-up frames are requested with `request_extra_frame()`.
Marking the canvas dirty from inside the render did not schedule another
idle pass, so thumbnails waited for the next mouse event.
- The selected texture is still centred on open, even when it is off
screen. The scroll reveal runs before the visibility check so that tile
is loaded on the next frame.
- Library validation no longer decodes pixels. The new
`png::is_decodable_8bit_png()` reads only the PNG header and accepts
8-bit grey or RGB/RGBA, which is what `decode_png()` and
`decode_colored_png()` take.
- Picker thumbnails are decoded without entering the decode cache
(`decode_height_texture(layer, keep_decoded = false)`). Only the 128 px
thumbnail is kept per library entry; the full-size decode is made
transiently and freed.

Tradeoff: the header-only check does not read pixel data, so a PNG with
a valid header but corrupt or truncated pixel data now passes validation
and fails when the texture is loaded. The previous full decode caught
that case.

Each thumbnail decode still runs at full size on the GUI thread, one at
a time. Peak memory per decode is unchanged; a streaming downscale would
be needed to reduce it.

# Screenshots/Recordings/Graphs

Old:


https://github.com/user-attachments/assets/03038532-396f-48c2-898c-aa06ef83e5f5


New:


https://github.com/user-attachments/assets/49fdc31a-ce1b-470a-88a7-4c6319f03b81



## Tests

Not yet run. Suggested checks:
- Open the texture picker with a library of many textures, and do not
move the mouse: tiles should fill in over a few frames without a visible
stall.
- Open the picker with a layer whose texture is far down the list: it
should be centred and loaded.
- Import a valid 8-bit grey and an RGB PNG, plus an unsupported one
(16-bit, palette, or grey-alpha): only the supported ones should be
accepted as-is, and the rest converted on load.
- Watch memory while scrolling through a large colour library: it should
not grow by the full decoded size of every texture shown.

<!--
> 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-10-16 18:11:46 +08:00
Lam Wei Lun cbd8ce98d5 Keep the texture picker redrawing while thumbnails load
Marking the canvas dirty from inside the render does not schedule another
idle pass, so deferred thumbnails waited for the next mouse event. Asking
for an extra frame does.
2026-10-16 17:55:37 +08:00
Lam Wei Lun 99714afc4c Keep picker thumbnails from retaining full-size texture decodes
The thumbnail is downscaled on upload, but the full decode it was made
from stayed in the decode cache for as long as the library entry lived.
Thumbnail decodes now skip the cache, so only the 128 px thumbnail is
kept per library texture.
2026-10-16 17:49:35 +08:00
Lam Wei Lun 3d07663ab2 Merge branch 'main' into fix/texture-picker-lazy-png-header 2026-10-16 15:41:04 +08:00
Lam Wei Lun 4149401c53 Keep the texture picker from stalling on open
Opening the texture picker decoded every texture in the expanded sections
on the GUI thread in one frame, each one twice at full size. Only the
visible tiles now load, at most a couple per frame, and the rest fill in
over the following frames.

The library validation no longer decodes the pixels: a header-only check,
is_decodable_8bit_png(), accepts exactly what the two decoders take.
2026-10-16 02:41:12 +08:00
32 changed files with 1081 additions and 216 deletions
+13
View File
@@ -288,6 +288,19 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
+50
View File
@@ -111,6 +111,54 @@ else
export CMAKE_POLICY_COMPAT=""
fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:"
echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -162,6 +210,7 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -264,6 +313,7 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
+3 -7
View File
@@ -812,7 +812,8 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole.
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
*
* Odd walls should always go after their enclosing wall polygons.
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
* like any other when printing inner to outer.
*
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
continue; // points are too far away from each other
if (here->is_odd || nearby->is_odd) {
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
order_requirements.emplace(std::make_pair(nearby, here));
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
order_requirements.emplace(std::make_pair(here, nearby));
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here));
} else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
+10
View File
@@ -3985,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
+45 -69
View File
@@ -120,7 +120,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@"
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
};
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -147,7 +153,13 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@"
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
};
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
return ret;
};
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos)
return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& placeholder = reserved_tag(tag);
size_t pos = gcode_line.find(placeholder);
while (pos != std::string::npos) {
gcode_line.replace(pos, placeholder.length(), value);
processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0)
@@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60;
// Replace print_time_total_sec
size_t pos = gcode_line.find(print_time_total_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%.2f", print_time_total_sec);
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
}
// Replace print_time_day
pos = gcode_line.find(print_time_day_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_day);
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
}
// Replace print_time_hour
pos = gcode_line.find(print_time_hour_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_hour);
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
}
// Replace print_time_minute
pos = gcode_line.find(print_time_minute_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_minute);
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
}
// Replace print_time_sec
pos = gcode_line.find(print_time_sec_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_sec);
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
}
// Replace used_filament_length
pos = gcode_line.find(used_filament_placeholder);
while (pos != std::string::npos) {
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
}
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
char buf[64];
sprintf(buf, "%.2f", used_filament_length);
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
}
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
replace_tag(ETags::Used_Filament_Length_Placeholder,
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
short_time("N/A"));
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
return processed;
};
+6
View File
@@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder,
Used_Filament_Length_Placeholder,
Normal_Print_Time_Placeholder,
Silent_Print_Time_Placeholder,
Total_Weight_Placeholder,
Total_Cost_Placeholder,
Total_Wipe_Tower_Cost_Placeholder,
Total_Wipe_Tower_Filament_Placeholder,
};
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
+29
View File
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
return decode_colored_png(stream, out_img);
}
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
{
static const constexpr int PNG_SIG_BYTES = 8;
struct ReadBufStream stream{pngbuf};
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
stream.read(sig.data(), PNG_SIG_BYTES);
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
return false;
PNGDescr dsc;
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!dsc.png)
return false;
dsc.info = png_create_info_struct(dsc.png);
if (!dsc.info)
return false;
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
return false;
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
bit_depth == 8 && (grey || colour);
}
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
// Based on https://www.lemoda.net/c/write-png/
+3
View File
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
bool is_png(const ReadBuf &pngbuf);
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
struct ReadBufStream: public IStream {
const ReadBuf &rbuf_ref;
size_t pos = 0;
+65 -24
View File
@@ -62,7 +62,12 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
bool is_thin_wall_hole = false;
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
bool is_narrow_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children;
@@ -572,7 +577,8 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop));
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner)
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
!perimeter_generator.outer_walls_first_for_brim())
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
}
else {
@@ -1690,13 +1696,24 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
if (i == 0 && ! holes[0].empty()) {
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0])
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
auto reach = [&last](double room) {
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
};
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
return intersection_pl(Polylines{ hole.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
};
// Only Inner/Outer/Inner orders narrow wall holes apart.
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
for (PerimeterGeneratorLoop &hole : holes[0]) {
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
}
}
//BBS: refer to superslicer
@@ -1835,18 +1852,16 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim
// TODO: add test for perimeter order
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
if (is_outer_wall_first ||
//BBS: always print outer wall first when there indeed has brim.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
(this->layer_id == 0 &&
(this->object_config->brim_type == BrimType::btOuterOnly ||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
// then the contour, then the gap fill between them, as Arachne does.
const PerimeterGeneratorLoops &island_loops = contours.front();
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
island_loops.front().children.front().is_narrow_wall_hole;
if (is_outer_wall_first || this->outer_walls_first_for_brim())
entities.reverse();
// Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
entities.reverse(); // reverse all entities - order them from external to internal
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2412,11 +2427,25 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) {
std::unordered_set<int> firstLevelIndices;
firstLevelIndices.insert(referenceIndex);
// Find first level touching perimeters
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
std::vector<int> outerIndices{referenceIndex};
std::vector<int> firstLevelTouchingIndices;
for (size_t next = 0; next < outerIndices.size(); ++next)
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
continue;
firstLevelTouchingIndices.push_back(idx);
if (!entities[idx].extrusion->is_odd)
continue;
for (size_t i = 0; i < entities.size(); ++i)
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
outerIndices.push_back(int(i));
}
std::stable_partition(outerIndices.begin(), outerIndices.end(),
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
// Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up
@@ -2427,8 +2456,10 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
}
// Insert first level perimeters into reordered list
reordered.push_back(entities[referenceIndex]);
includedIndices.insert(referenceIndex);
for (int idx : outerIndices) {
reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) {
@@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner;
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
}
if (is_outer_wall_first) {
start_perimeter = 0;
@@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
}
bool PerimeterGenerator::outer_walls_first_for_brim() const
{
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
}
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
{
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
}
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
+6
View File
@@ -164,6 +164,8 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const;
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
bool outer_walls_first_for_brim() const;
private:
std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -183,6 +185,10 @@ private:
Polygons m_lower_slices_polygons;
};
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
}
#endif
+1
View File
@@ -2293,6 +2293,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue;
}
extend_default_config_length(config, false, {});
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config));
Preset::normalize(preset->config);
+8 -9
View File
@@ -17,6 +17,7 @@
#include "I18N.hpp"
#include "Layer.hpp"
#include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp"
@@ -1464,15 +1465,13 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
//to make the wall order of first layer to be outer-first
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
// takes part in the same first-layer wall order rule.
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
if (opt_key == "brim_type" || opt_key == "brim_width") {
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
auto outer_walls_first = [](const ConfigOptionResolver &config) {
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
config.option<ConfigOptionFloat>("brim_width")->value);
};
if (outer_walls_first(old_config) != outer_walls_first(new_config))
steps.emplace_back(posPerimeters);
}
} else if (
+3 -6
View File
@@ -707,11 +707,13 @@ void tree_supports_generate_paths(
false);
}
if (eec) {
if (!eec->empty()) {
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
}
}
}
void fill_expolygons_with_sheath_generate_paths(
ExtrusionEntitiesPtr &dst,
@@ -1825,12 +1827,7 @@ void generate_support_toolpaths(
no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
// Orca: A special case for the hollow Organic supports
// Orca: If the tree supports are too tall, use a double wall to make it stronger
SupportParameters support_params2 = support_params;
if (support_layer.print_z > 100.0)
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
done = true;
}
if (! done)
+10 -22
View File
@@ -2149,7 +2149,9 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value;
// Reinforce branches moving more than half a support-line width between layers.
// Square the threshold for comparison with the squared XY movement below.
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
// Belt floor: the first object layer is not on a flat bed — it rests on the
// tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2201,7 +2203,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
bool need_extra_wall = false;
ExPolygons extra_wall_areas;
ExPolygons collision_sharp_tails;
ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2279,7 +2281,6 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2294,6 +2295,10 @@ void TreeSupport::draw_circles()
}
}
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
append(extra_wall_areas, area);
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2530,7 +2535,8 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
}
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3433,11 +3439,6 @@ void TreeSupport::smooth_nodes()
}
}
float max_move = scale_(m_object_config->support_line_width / 2);
// if the branch is very tall, the tip also needs extra wall
float thresh_tall_branch = 100;
float thresh_dist_to_top = 30;
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue;
@@ -3447,20 +3448,17 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii;
std::vector<SupportNode *> branch;
SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius);
branch.push_back(p_node->child);
total_height += p_node->child->height;
}
do {
pts.push_back(p_node->position);
radii.push_back(p_node->radius);
branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent;
} while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue;
@@ -3479,9 +3477,6 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true;
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
branch[i]->need_extra_wall = true;
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
}
}
@@ -3489,13 +3484,6 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1);
std::swap(radii, radii1);
}
else {
// interpolate need_extra_wall in the end
for (size_t i = 1; i < branch.size() - 1; i++) {
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
branch[i]->need_extra_wall = true;
}
}
}
}
}
-1
View File
@@ -135,7 +135,6 @@ struct SupportNode
TreeNodeType type = eCircle;
bool is_corner = false;
bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false;
bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+3 -1
View File
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
{
DecodedHeightTexture result;
if (layer.empty())
@@ -351,6 +351,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
}
}
if (keep_decoded) {
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
@@ -359,6 +360,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
}
}
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
// because `preview_adjust` is an edit to the image while `smoothing` is a live low-pass over whatever
+3 -2
View File
@@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
// as it did when the importer flattened it to grey on the way in. Returns an empty
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
+7
View File
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
// is made absolute and lexically normalized first, because the prefix disables normalisation.
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
// or when an absolute path cannot be formed, the input is returned unchanged.
boost::filesystem::path extended_path(const boost::filesystem::path& path);
} // namespace Slic3r
#if WIN32
+53 -8
View File
@@ -17,6 +17,7 @@
#if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp"
#include "boost/nowide/convert.hpp"
#endif
#include "I18N.hpp"
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
FILE *f = nullptr;
#if defined(_MSC_VER) || defined(__MINGW64__)
f = boost::nowide::fopen(fname, mode);
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
const std::wstring wide_path = boost::nowide::widen(fname);
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
f = boost::nowide::fopen(extended.c_str(), mode);
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
f = fopen64(fname, mode);
#else
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
}
return ret;
}
#ifdef WIN32
// mz_file_write_func writing an extracted entry into an already-open FILE*.
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
{
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
}
#endif
}
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
{
if (!zip)
return false;
#ifdef WIN32
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
if (!file) {
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
return false;
}
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
const bool closed = std::fclose(file) == 0;
if (!written || !closed) {
if (written && !closed)
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
// Drop the partial entry rather than leave a truncated file behind.
boost::system::error_code ec;
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
return false;
}
return true;
#else
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
#endif
}
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
{
if (stat.m_is_utf8)
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
}
const std::string dest_file = dest_dir + "/" + stat.m_filename;
try {
if (stat.m_is_directory) {
// Use the extended-length form for every filesystem call so entries under a destination
// past MAX_PATH are created even with LongPathsEnabled off.
const boost::filesystem::path dest_path(dest_file);
if (!boost::filesystem::exists(dest_path))
boost::filesystem::create_directories(dest_path);
const boost::filesystem::path io_dest = extended_path(dest_path);
if (stat.m_is_directory) {
if (!boost::filesystem::exists(io_dest))
boost::filesystem::create_directories(io_dest);
continue;
}
if (stat.m_uncomp_size == 0) {
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
continue;
}
// Replace a symlink at the destination rather than writing through it.
const boost::filesystem::path dest_path(dest_file);
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
boost::filesystem::remove(dest_path);
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
boost::filesystem::remove(io_dest);
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
close_zip_reader(&archive);
return false;
+5
View File
@@ -1,6 +1,7 @@
#ifndef MINIZ_EXTENSION_HPP
#define MINIZ_EXTENSION_HPP
#include <boost/filesystem/path.hpp>
#include <string>
#include <miniz.h>
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
class MZ_Archive {
public:
+40
View File
@@ -2254,5 +2254,45 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
return mod >= 0 && mod < count;
}
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
#ifdef WIN32
namespace fs = boost::filesystem;
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
// be absolutized or normalized): leave it alone.
{
fs::path prefix_candidate = path;
const std::wstring native = prefix_candidate.make_preferred().wstring();
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
return fs::path(native);
}
fs::path absolute = path;
if (!absolute.is_absolute()) {
boost::system::error_code ec;
absolute = fs::absolute(absolute, ec);
if (ec)
return path; // can't form a valid prefixed path from a relative one
}
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
while (absolute.has_relative_path() && absolute.filename() == ".")
absolute = absolute.parent_path();
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
absolute = absolute.lexically_normal();
// Extended paths only recognize backslash separators.
const std::wstring native = absolute.make_preferred().wstring();
if (native.empty())
return path;
if (native.rfind(L"\\\\", 0) == 0)
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
#else
return path;
#endif
}
}; // namespace Slic3r
@@ -174,6 +174,10 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048;
// The full-size image is only ever adjusted once, on commit.
constexpr int ADJUST_PREVIEW_MAX_PX = 384;
// Library textures the texture picker decodes per frame. Each decode runs on the GUI thread, so the
// rest of the visible tiles fill in over the following frames instead of in one stall.
constexpr int PICKER_LOADS_PER_FRAME = 2;
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own
// cost growing to matter: it is a ring around a perimeter, not an area.
@@ -4118,7 +4122,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge
if (entry.image_data) {
TextureDisplacementLayer probe;
probe.image_data = entry.image_data;
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe));
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
if (!entry.thumbnail)
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
} else {
@@ -4331,6 +4335,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false;
bool download_clicked = false;
int loads_left = PICKER_LOADS_PER_FRAME;
bool loads_deferred = false;
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4348,11 +4354,30 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
};
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
// name, the user's own most recently used first - the order texture_library() produced); a user tile
// carries a remove button on hover. Returns how many it drew.
// carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
int n = 0;
for (const TextureLibraryEntry *entry : entries) {
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
const ImVec2 rect_min = tile_pos(origin, n++);
const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile);
const bool current = entry->path == layer->path;
// The selected tile is centred even when it is off screen, so it is drawn and loaded next frame.
if (current && m_picker_reveal_selected) {
ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f);
m_parent.request_extra_frame();
}
if (!ImGui::IsRectVisible(rect_min, rect_max))
continue;
// Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames.
const bool cached = m_library_textures.count(entry->path) != 0;
if (!cached && loads_left == 0) {
loads_deferred = true;
dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
continue;
}
if (!cached)
--loads_left;
ImGui::SetCursorScreenPos(rect_min);
ImGui::PushID(entry->path.c_str());
const LibraryTexture *tex = get_library_texture(entry->path);
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
@@ -4375,7 +4400,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (removable)
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
const bool current = entry->path == layer->path;
const bool hovered = ImGui::IsItemHovered();
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
// fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
@@ -4384,9 +4408,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
}
// Centred, as far as the scroll range allows, so the textures around it are in view too.
if (current && m_picker_reveal_selected)
ImGui::SetScrollFromPosY(0.5f * (a.y + b.y) - ImGui::GetWindowPos().y, 0.5f);
if (hovered)
panel_tooltip(entry->name);
@@ -4575,6 +4596,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos();
}
if (loads_deferred)
m_parent.request_extra_frame();
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false;
ImGui::EndChild();
+27 -15
View File
@@ -474,17 +474,14 @@ void Selection::add_curr_plate()
if (!m_valid)
return;
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (volume_idxs.empty())
return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!"));
m_mode = Instance;
clear();
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance_totally(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs);
}
}
update_type();
this->set_bounding_boxes_dirty();
@@ -511,20 +508,14 @@ void Selection::remove_curr_plate()
if (!m_valid)
return;
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
if (plate->empty())
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (volume_idxs.empty())
return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All"));
m_mode = Instance;
clear();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs);
}
}
update_type();
this->set_bounding_boxes_dirty();
@@ -2270,6 +2261,27 @@ std::vector<unsigned int> Selection::get_volume_idxs_from_volume(unsigned int ob
return idxs;
}
std::vector<unsigned int> Selection::get_volume_idxs_from_plate(const int plate_idx) const
{
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx);
std::vector<unsigned int> idxs;
if (!plate)
return idxs;
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); obj_idx++) {
const ModelObject* obj = m_model->objects[obj_idx];
for (int inst_idx = 0; inst_idx < (int)obj->instances.size(); inst_idx++) {
// on this plate or overlapping it in some way (fully inside, oversized or partly outside)
if (plate->contain_instance(obj_idx, inst_idx) || plate->intersect_instance(obj_idx, inst_idx)) {
std::vector<unsigned int> inst_idxs = get_volume_idxs_from_instance(obj_idx, inst_idx);
idxs.insert(idxs.end(), inst_idxs.begin(), inst_idxs.end());
}
}
}
return idxs;
}
std::vector<unsigned int> Selection::get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const
{
std::vector<unsigned int> idxs;
+2
View File
@@ -409,6 +409,8 @@ public:
std::vector<unsigned int> get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const;
// returns the idx of the volume corresponding to the volume with the given idxs
std::vector<unsigned int> get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const;
// returns the list of idxs of the volumes of all instances on the given plate or overlapping it. (fully inside, oversized or partly outside)
std::vector<unsigned int> get_volume_idxs_from_plate(const int plate_idx) const;
// returns the list of idxs of the volumes contained in the selection but not in the given list
std::vector<unsigned int> get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const;
// returns the list of idxs of the volumes contained in the given list but not in the selection
+3 -15
View File
@@ -223,23 +223,11 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
// the height and whose RGB is available to colour the model). Anything else is converted on load.
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
// Anything else is converted on load.
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{
if (bytes.empty())
return false;
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
if (!png::is_png(rbuf))
return false;
png::ImageGreyscale gray;
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
return true;
png::ImageColorscale color;
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
color.bytes_per_pixel >= 3 &&
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
}
std::vector<TextureLibraryEntry> g_library;
+6 -17
View File
@@ -34,10 +34,6 @@
#include <unordered_set>
#include <vector>
#ifdef WIN32
#include <boost/locale/encoding_utf.hpp>
#endif
namespace Slic3r {
const char* const INSTALL_STATE_FILE = ".install_state.json";
@@ -665,7 +661,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::create_directories(destination, ec);
fs::create_directories(extended_path(destination), ec);
if (ec) {
error = "Failed to create plugin staging directory: " + ec.message();
return false;
@@ -709,7 +705,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
const fs::path output_path = destination / relative_path;
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
fs::create_directories(output_path, ec);
fs::create_directories(extended_path(output_path), ec);
if (ec) {
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
return false;
@@ -717,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
continue;
}
fs::create_directories(output_path.parent_path(), ec);
fs::create_directories(extended_path(output_path.parent_path()), ec);
if (ec) {
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
return false;
}
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
return false;
}
const std::string encoded_output_path = encode_path(output_path.string().c_str());
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
#ifdef WIN32
if (!extracted) {
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
}
#endif
if (!extracted) {
// Shared with the rest of the archive code: long-path capable and binary-safe.
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
error = "Failed to extract plugin zip entry " + relative_key + ": " +
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
+382
View File
@@ -22,6 +22,7 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include <limits>
#include <string>
@@ -994,3 +995,384 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime
CHECK(layers[i].holes_ccw.front() == ccw);
}
}
namespace {
// The outer radius of round_tube().
const double tube_radius = 11.5;
// Every setting the tube walls below are measured against. With 0.5mm walls a 1mm tube wall holds only the two
// outer walls, touching, and every further 0.5mm makes room for one more inner wall.
DynamicPrintConfig tube_config(const char *wall_generator, int wall_loops)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "wall_loops", wall_loops },
{ "wall_sequence", "inner wall/outer wall" },
{ "wall_direction", "ccw" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "nozzle_diameter", "0.4" },
{ "outer_wall_line_width", 0.5 },
{ "inner_wall_line_width", 0.5 },
{ "precise_outer_wall", true },
{ "detect_thin_wall", false },
{ "only_one_wall_top", false },
{ "only_one_wall_first_layer", false },
{ "brim_type", "no_brim" },
{ "detect_overhang_wall", true },
{ "overhang_reverse", false },
{ "overhang_reverse_threshold", "0%" },
});
return config;
}
// A 2mm tall round tube whose wall is `wall` mm thick.
Print &round_tube(Print &print, Model &model, const DynamicPrintConfig &config, double wall)
{
ModelObject *object = model.add_object();
object->name = "round_tube.stl";
object->add_volume(make_cylinder(tube_radius, 2.), ModelVolumeType::MODEL_PART, false);
object->add_volume(make_cylinder(tube_radius - wall, 2.), ModelVolumeType::NEGATIVE_VOLUME, false);
object->add_instance();
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
return print;
}
// The side of the tube wall a wall loop lies on. The centre is the middle of the tube wall, where Arachne lays the
// inner wall both sides share when the tube wall is too narrow for a pair of them.
enum class TubeSide { Contour, Hole, Centre };
struct TubeLoop {
TubeSide side;
int inset;
bool ccw;
};
// The wall loops of every layer of round_tube() in extrusion order.
std::vector<std::vector<TubeLoop>> tube_loops(const Print &print, double wall)
{
const double centre = tube_radius - 0.5 * wall;
std::vector<std::vector<TubeLoop>> out;
for (const Layer *layer : print.objects().front()->layers()) {
const Vec2d axis = get_extents(layer->lslices).center().cast<double>();
std::vector<TubeLoop> &loops = out.emplace_back();
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *entity : region->perimeters.flatten().entities) {
if (! entity->is_loop())
continue;
const ExtrusionLoop *loop = static_cast<const ExtrusionLoop*>(entity);
const Polygon polygon = loop->polygon();
double radius = 0.;
for (const Point &p : polygon.points)
radius += unscaled<double>((p.cast<double>() - axis).norm());
radius /= double(polygon.points.size());
const TubeSide side = radius > centre + 0.1 ? TubeSide::Contour : radius < centre - 0.1 ? TubeSide::Hole : TubeSide::Centre;
loops.push_back({ side, loop->inset_idx, polygon.is_counter_clockwise() });
}
}
return out;
}
// The insets one side of a wall is printed in, given its deepest inset: inside out for Inner/Outer, outside in for
// Outer/Inner, and for Inner/Outer/Inner the third wall onwards inside out, then the outer wall, then the first inner
// wall. Inner/Outer/Inner prints its first layer Inner/Outer, and outside in when there is no third wall.
std::vector<int> wall_sequence_insets(const std::string &wall_sequence, bool first_layer, int deepest)
{
std::vector<int> insets;
if (wall_sequence == "inner wall/outer wall" || (wall_sequence == "inner-outer-inner wall" && first_layer)) {
for (int inset = deepest; inset >= 0; -- inset)
insets.push_back(inset);
} else if (wall_sequence == "outer wall/inner wall" || deepest < 2) {
for (int inset = 0; inset <= deepest; ++ inset)
insets.push_back(inset);
} else {
for (int inset = deepest; inset >= 2; -- inset)
insets.push_back(inset);
insets.push_back(0);
insets.push_back(1);
}
return insets;
}
// Whether a wall of round_tube() runs with the contour: all but the walls of the hole, and those too when the hole
// is a thin wall hole, its outer wall touching the contour's with nothing between them.
bool runs_with_contour(const TubeLoop &loop, bool thin_wall_hole)
{
return loop.side != TubeSide::Hole || thin_wall_hole;
}
// The insets of the walls on one side of a round_tube() layer, the shared centre line included, in extrusion order.
std::vector<int> side_insets(const std::vector<TubeLoop> &loops, TubeSide side)
{
std::vector<int> insets;
for (const TubeLoop &loop : loops)
if (loop.side == side || loop.side == TubeSide::Centre)
insets.push_back(loop.inset);
return insets;
}
// The walls of a round_tube() layer in extrusion order, as side and inset: "c0 c1 h0 h1" is the contour's outer wall,
// then its first inner wall, then the same for the hole, and "m" marks the wall in the middle both sides share.
std::string wall_order(const std::vector<TubeLoop> &loops)
{
std::string out;
for (const TubeLoop &loop : loops)
out += std::string(out.empty() ? "" : " ") + (loop.side == TubeSide::Contour ? "c" : loop.side == TubeSide::Hole ? "h" : "m") +
std::to_string(loop.inset);
return out;
}
} // namespace
// Each side of a tube wall prints its walls in the order the wall sequence asks for, from two walls a side up to
// four. The inner wall Arachne lays in the middle of a wall too narrow for a pair of them is an inner wall of both
// sides; Classic fills that middle with gap fill instead, so it has one wall less a side there.
TEST_CASE("Each side of a tube wall prints its walls in the wall sequence order", "[Perimeters]")
{
struct TubeWall {
double wall;
int wall_loops;
// Walls a side, a shared inner wall included.
int arachne_walls;
int classic_walls;
};
const TubeWall tube = GENERATE(values<TubeWall>({
{ 1.5, 3, 2, 1 }, { 2.0, 2, 2, 2 }, { 2.5, 3, 3, 2 }, { 3.0, 3, 3, 3 }, { 4.0, 4, 4, 4 } }));
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(tube.wall, tube.wall_loops, wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, tube.wall_loops);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, tube.wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, tube.wall);
// 2mm at 0.2mm layers.
REQUIRE(layers.size() == 10);
const int walls = std::string(wall_generator) == "arachne" ? tube.arachne_walls : tube.classic_walls;
for (size_t i = 0; i < layers.size(); ++ i) {
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(int(insets.size()) == walls);
CHECK(insets == wall_sequence_insets(wall_sequence, i == 0, walls - 1));
}
}
}
// The outer wall of a thin wall hole touches the contour's with nothing between them. The hole is printed first, so
// the contour leans on it, unless the wall sequence puts the outer walls first.
TEST_CASE("A thin wall hole is printed before the contour unless the outer walls go first", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, 1.);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.);
REQUIRE(layers.size() == 10);
const TubeSide first = wall_sequence == "outer wall/inner wall" ? TubeSide::Contour : TubeSide::Hole;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(layers[i].size() == 2);
REQUIRE(layers[i][0].side != layers[i][1].side);
CHECK(layers[i].front().side == first);
}
}
// A 1.5mm tube wall leaves Inner/Outer/Inner no third wall to sandwich, only the inner wall Arachne lays in the middle
// for both sides, or Classic's gap fill. Past the first layer it prints the hole first, then the contour, then the
// middle.
TEST_CASE("Inner/Outer/Inner prints the hole of a narrow tube wall first and the middle last", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", "inner-outer-inner wall");
Print print;
Model model;
round_tube(print, model, config, 1.5);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.5);
REQUIRE(layers.size() == 10);
// Classic's gap fill is not a wall loop, and goes after the walls.
const std::string expected = std::string(wall_generator) == "arachne" ? "h0 c0 m1" : "h0 c0";
for (size_t i = 1; i < layers.size(); ++ i) {
CAPTURE(i);
CHECK(wall_order(layers[i]) == expected);
}
}
// The contour's walls run in the wall direction and the hole's against it, except that the walls of a thin wall
// hole run with the contour, as does the inner wall Arachne shares between both sides of a 1.5mm tube wall.
TEST_CASE("Tube walls run in the wall direction and hole walls against it", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 2.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_direction = GENERATE("ccw", "cw");
CAPTURE(wall, wall_generator, wall_direction);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_direction", wall_direction);
Print print;
Model model;
round_tube(print, model, config, wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
const bool thin_wall_hole = wall == 1.;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Contour; }));
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Hole; }));
for (const TubeLoop &loop : layers[i]) {
CAPTURE(int(loop.side), loop.inset);
CHECK(loop.ccw == (runs_with_contour(loop, thin_wall_hole) == (wall_direction == "ccw")));
}
}
}
// Reverse on even with no threshold reverses every wall of every other layer, or only the inner walls when it is
// restricted to them, and leaves the order of the walls alone.
TEST_CASE("Reverse on even reverses the tube walls of every other layer", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const bool reverse_internal_only = GENERATE(false, true);
CAPTURE(wall, wall_generator, reverse_internal_only);
auto slice = [&](bool overhang_reverse) {
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "overhang_reverse", overhang_reverse },
{ "overhang_reverse_internal_only", reverse_internal_only },
});
Print print;
Model model;
round_tube(print, model, config, wall);
return tube_loops(print, wall);
};
const std::vector<std::vector<TubeLoop>> plain = slice(false);
const std::vector<std::vector<TubeLoop>> reversed = slice(true);
REQUIRE(plain.size() == 10);
REQUIRE(reversed.size() == plain.size());
for (size_t i = 0; i < plain.size(); ++ i) {
CAPTURE(i);
REQUIRE(reversed[i].size() == plain[i].size());
for (size_t j = 0; j < plain[i].size(); ++ j) {
CAPTURE(j);
CHECK(reversed[i][j].side == plain[i][j].side);
CHECK(reversed[i][j].inset == plain[i][j].inset);
const bool flips = i % 2 == 1 && (! reverse_internal_only || plain[i][j].inset > 0);
CHECK(reversed[i][j].ccw == (plain[i][j].ccw != flips));
}
}
}
namespace {
DynamicPrintConfig brim_tube_config(const char *wall_generator, const std::string &wall_sequence, const char *brim_type, double brim_width)
{
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "wall_sequence", wall_sequence },
{ "brim_type", brim_type },
{ "brim_width", brim_width },
});
return config;
}
} // namespace
// A brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the brim runs
// straight on into the outer wall: each side of the tube wall outside in, and the contour before a thin wall hole.
// Brims that may leave the outer walls bare keep the wall sequence, and so do the layers above.
TEST_CASE("A brim against the outer walls makes the first layer print them first", "[Perimeters]")
{
struct Brim {
const char *type;
double width;
bool outer_walls_first;
};
const Brim brim = GENERATE(values<Brim>({
{ "no_brim", 5., false }, { "inner_only", 5., false }, { "auto_brim", 5., false }, { "outer_only", 0., false },
{ "outer_only", 5., true }, { "outer_and_inner", 5., true }, { "leading_edge_only", 5., true } }));
const double wall = GENERATE(1.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "inner-outer-inner wall");
CAPTURE(brim.type, brim.width, wall, wall_generator, wall_sequence);
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, brim.type, brim.width), wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
// The thin wall hole has its outer wall a side, the 3mm tube wall three walls a side.
const size_t walls = wall == 1. ? 1 : 3;
for (const size_t i : { size_t(0), size_t(1) }) {
const bool outer_walls_first = brim.outer_walls_first && i == 0;
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(insets.size() == walls);
CHECK(insets == wall_sequence_insets(outer_walls_first ? "outer wall/inner wall" : wall_sequence, i == 0, int(walls) - 1));
}
if (wall == 1.)
CHECK(layers[i].front().side == (outer_walls_first ? TubeSide::Contour : TubeSide::Hole));
}
}
// Changing the brim of a sliced print regenerates the walls when the first layer's wall order changes with it, so
// the print gets the walls a slice from scratch with the new brim gets.
TEST_CASE("Changing the brim updates the wall order of the first layer", "[Perimeters]")
{
struct BrimChange {
const char *from_type;
double from_width;
const char *to_type;
double to_width;
};
const BrimChange change = GENERATE(values<BrimChange>({
{ "outer_only", 0., "outer_only", 5. }, { "outer_only", 5., "outer_only", 0. },
{ "no_brim", 5., "outer_and_inner", 5. }, { "outer_and_inner", 5., "inner_only", 5. } }));
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(change.from_type, change.from_width, change.to_type, change.to_width, wall_generator);
const std::string wall_sequence = "inner wall/outer wall";
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, change.from_type, change.from_width), 3.);
const std::string before = wall_order(tube_loops(print, 3.).front());
print.apply(model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width));
print.process();
const std::string after = wall_order(tube_loops(print, 3.).front());
Print fresh_print;
Model fresh_model;
round_tube(fresh_print, fresh_model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width), 3.);
const std::string fresh = wall_order(tube_loops(fresh_print, 3.).front());
// The change does turn the first layer around.
REQUIRE(before != fresh);
CHECK(after == fresh);
}
+59
View File
@@ -735,6 +735,65 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
CHECK(per_layer.find(";Layer:399 ") != std::string::npos);
}
// The final print statistics are known only after the whole G-code is generated, yet every custom G-code,
// the file header included, gets the same values as filename_format.
TEST_CASE("Print statistics in custom G-code match the final print statistics", "[Print][Regression]")
{
const std::string gcode_key = GENERATE(as<std::string>{}, "file_start_gcode", "machine_start_gcode", "machine_end_gcode");
// Bambu printers use another tag table and G-code layout.
const bool is_bbl = GENERATE(false, true);
CAPTURE(gcode_key, is_bbl);
const std::vector<std::string> number_keys = { "total_weight", "used_filament", "total_cost", "total_wipe_tower_cost",
"total_wipe_tower_filament" };
const std::vector<std::string> time_keys = { "print_time", "normal_print_time", "silent_print_time" };
std::string templ;
for (const std::vector<std::string> *keys : { &number_keys, &time_keys })
for (const std::string &key : *keys)
templ += "; stat " + key + "={" + key + "}\n";
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ gcode_key, templ },
{ "filament_density", "1.25" },
{ "filament_cost", "20" },
{ "time_cost", "2" },
});
Print print;
Model model;
init_print({ cube(20) }, print, model, config);
print.is_BBL_printer() = is_bbl;
std::string gcode;
REQUIRE_NOTHROW(gcode = Slic3r::Test::gcode(print));
// Match whole lines only: the config block repeats the template on a single line, and Bambu printers
// write that block before the start G-code.
const std::string lines = "\n" + gcode;
const auto value_of = [&lines](const std::string &key) {
const std::string prefix = "\n; stat " + key + "=";
const size_t start = lines.find(prefix);
REQUIRE(start != std::string::npos);
const size_t begin = start + prefix.size();
return lines.substr(begin, lines.find('\n', begin) - begin);
};
// The same statistics filename_format is expanded with.
DynamicConfig stats = print.print_statistics().config();
// A 20mm cube with filament_density 1.25 weighs something, unlike the running extruded_weight_total in the header.
CHECK(stats.opt_float("total_weight") > 0.);
for (const std::string &key : number_keys) {
CAPTURE(key);
const std::string value = value_of(key);
REQUIRE(value.find('@') == std::string::npos);
// Written with two decimals.
CHECK_THAT(std::stod(value), Catch::Matchers::WithinAbs(stats.opt_float(key), 0.006));
}
for (const std::string &key : time_keys) {
CAPTURE(key);
CHECK(value_of(key) == stats.opt_string(key));
}
}
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{
Print print;
+50
View File
@@ -18,6 +18,7 @@
#include <fstream>
#include <ios>
#include <iterator>
#include <libslic3r/Utils.hpp>
#include <miniz.h>
#include <string>
#include <utility>
@@ -176,6 +177,55 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
}
TEST_CASE("Confined extraction writes files under a target path longer than MAX_PATH", "[MinizExtension]")
{
ScopedTemporaryDir tmp;
const fs::path zip_file = tmp.path() / "bundle.zip";
// Directory entries are explicit: extract_archive_confined recreates directories only for them.
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"},
{"vendor/machine/printer.json", "{\"b\":2}"}});
// One 240-char component keeps every name below the NTFS 255 limit while pushing the full
// target path past the 260-char MAX_PATH the Win32 APIs enforce when LongPathsEnabled is off.
const fs::path target = tmp.path() / std::string(240, 'd');
REQUIRE(target.string().size() > 260);
fs::create_directories(extended_path(target));
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
// Verify through extended-length paths, otherwise the checks themselves are bounded by MAX_PATH.
CHECK(fs::is_directory(extended_path(target / "vendor")));
CHECK(read_file(extended_path(target / "vendor.json")) == "{\"a\":1}");
CHECK(read_file(extended_path(target / "vendor" / "machine" / "printer.json")) == "{\"b\":2}");
// The temporary dir guard cannot remove a deep tree on Windows, so drop it here.
boost::system::error_code ec;
fs::remove_all(extended_path(target), ec);
CHECK_FALSE(ec);
}
TEST_CASE("A zip archive under a path longer than MAX_PATH can be written and read", "[MinizExtension]")
{
ScopedTemporaryDir tmp;
// Create the long directory with the extended-length path, then exercise the archive
// reader/writer against an archive whose own path is past MAX_PATH.
const fs::path long_dir = tmp.path() / std::string(240, 'z');
fs::create_directories(extended_path(long_dir));
const fs::path zip_file = long_dir / "bundle.zip";
REQUIRE(zip_file.string().size() > 260);
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
const fs::path target = tmp.path() / "out";
fs::create_directories(target);
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
boost::system::error_code ec;
fs::remove_all(extended_path(long_dir), ec);
CHECK_FALSE(ec);
}
#ifndef _WIN32
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
{
@@ -985,6 +985,37 @@ TEST_CASE("A user filament synced to the cloud and back keeps its values and uns
}
}
TEST_CASE("A project filament storing one value for a per-variant key applies it to every variant", "[Preset][Bundle][Regression]")
{
PresetCollection filaments(Preset::TYPE_FILAMENT, Preset::filament_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
Preset &parent = add_inmemory_preset(filaments, "Parent Filament");
parent.config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
parent.config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({"parent standard", "parent high flow"}));
parent.config.set_deserialize_strict("pressure_advance", "0.02,0.03");
parent.is_system = true;
// A filament saved before these keys became per-variant lists both variants but stores each key once.
const std::string model = "0.025,15.6,5000";
DynamicPrintConfig config;
config.set_key_value(BBL_JSON_KEY_INHERITS, new ConfigOptionString("Parent Filament"));
config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({model}));
config.set_deserialize_strict("pressure_advance", "0.016");
auto embedded = std::make_unique<Preset>(Preset::TYPE_FILAMENT, "Project Filament", false);
embedded->config = std::move(config);
embedded->is_project_embedded = true;
std::vector<Preset *> project_presets{embedded.get()};
PresetsConfigSubstitutions substitutions;
filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
const Preset *loaded = filaments.find_preset("Project Filament");
REQUIRE(loaded != nullptr);
CHECK(loaded->config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values == std::vector<std::string>{model, model});
check_double_vector(loaded->config.option<ConfigOptionFloats>("pressure_advance")->values, {0.016, 0.016});
}
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
+46
View File
@@ -521,3 +521,49 @@ TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]")
CHECK_FALSE(ascii_iequals("[", "{"));
CHECK_FALSE(ascii_iequals("@", "`"));
}
TEST_CASE("extended_path turns a Windows path into its long-path form", "[Utils]") {
#ifdef _WIN32
namespace fs = boost::filesystem;
SECTION("a normal absolute drive path is prefixed") {
CHECK(extended_path(fs::path(R"(C:\models\model.3mf)")).wstring() == LR"(\\?\C:\models\model.3mf)");
}
SECTION("separators are made native") {
CHECK(extended_path(fs::path("C:/models/model.3mf")).wstring() == LR"(\\?\C:\models\model.3mf)");
}
SECTION("dot segments are collapsed before prefixing") {
// The "\\?\" prefix disables Win32 normalisation, so "." and ".." must be gone beforehand.
CHECK(extended_path(fs::path(R"(C:\models\..\model.3mf)")).wstring() == LR"(\\?\C:\model.3mf)");
}
SECTION("a trailing separator is dropped, not turned into a dot element") {
CHECK(extended_path(fs::path(R"(C:\models\)")).wstring() == LR"(\\?\C:\models)");
}
SECTION("a UNC path becomes an extended UNC path") {
CHECK(extended_path(fs::path(R"(\\server\share\model.3mf)")).wstring() == LR"(\\?\UNC\server\share\model.3mf)");
}
SECTION("an already extended path is returned unchanged") {
const fs::path extended(R"(\\?\C:\models\model.3mf)");
CHECK(extended_path(extended) == extended);
}
SECTION("a device path is returned unchanged") {
const fs::path device(R"(\\.\C:\models\model.3mf)");
CHECK(extended_path(device) == device);
}
SECTION("a bare device path without a root directory is returned unchanged") {
// Not "absolute" by Boost's rules, so it must be recognized before absolutizing.
const fs::path device(R"(\\.\PhysicalDrive0)");
CHECK(extended_path(device) == device);
}
SECTION("a relative path is completed against the working directory") {
const fs::path result = extended_path(fs::path("model.3mf"));
CHECK(result.wstring().rfind(L"\\\\?\\", 0) == 0);
CHECK(result.filename() == fs::path("model.3mf"));
}
#else
SECTION("non-Windows returns the path unchanged") {
const boost::filesystem::path p = boost::filesystem::temp_directory_path() / "model.3mf";
CHECK(extended_path(p) == p);
}
#endif
}
+69
View File
@@ -3,6 +3,7 @@
#include <catch2/catch_all.hpp>
#include <ios>
#include <libslic3r/Utils.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
@@ -10,13 +11,17 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "catch2/catch_message.hpp"
#include "plugin_test_utils.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <fstream>
#include <string>
#include <libslic3r/Zipper.hpp>
using namespace Slic3r;
namespace fs = boost::filesystem;
@@ -31,6 +36,14 @@ fs::path write_py_file(const fs::path& dir, const std::string& filename, const s
return p;
}
// The shared production helper: makes the path absolute and lexically normalized and, on
// Windows, prefixes it with the extended-length "\\?\" form, so the test can create, verify and
// clean up a tree past MAX_PATH even with LongPathsEnabled off. Identity on other platforms.
fs::path to_extended_path(const fs::path& p)
{
return Slic3r::extended_path(p);
}
} // namespace
TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]")
@@ -184,3 +197,59 @@ TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginIns
REQUIRE(read_install_state(plugin_dir, scanned));
CHECK(scanned.installed_version == "1.0.0");
}
TEST_CASE("extract_zip_to_directory writes entries under a destination longer than MAX_PATH",
"[PluginInstall][PluginFsUtils]")
{
ScopedTemporaryDir dir("deep-extract");
const fs::path wheel = dir.path() / "deep_pkg-1.0-py3-none-any.whl";
const std::string payload = "print('hello from a deep path')\n";
{
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
zipper.add_entry("pkg/", "", 0); // explicit directory entry
zipper.add_entry("pkg/module.py", payload.data(), payload.size());
zipper.finalize();
}
// A single 240-char component keeps every filename below the NTFS 255 limit while pushing the
// full destination (temp dir + component) past the 260-char MAX_PATH the Win32 APIs enforce
// when LongPathsEnabled is off.
const fs::path destination = dir.path() / std::string(240, 'd');
REQUIRE(destination.string().size() > 260);
std::string error;
REQUIRE(extract_zip_to_directory(wheel, destination, error));
INFO("extract error: " << error);
CHECK(error.empty());
// Verify through a long-path-capable path, otherwise the check itself is bounded by MAX_PATH.
const fs::path module_py = destination / "pkg" / "module.py";
REQUIRE(fs::exists(to_extended_path(module_py)));
REQUIRE(fs::is_regular_file(to_extended_path(module_py)));
// Remove the deep subtree here: ScopedTemporaryDir's plain remove_all cannot reach past
// MAX_PATH on Windows.
boost::system::error_code ec;
fs::remove_all(to_extended_path(destination), ec);
CHECK_FALSE(ec);
}
TEST_CASE("extract_zip_to_directory rejects an entry that escapes the destination",
"[PluginInstall][PluginFsUtils]")
{
ScopedTemporaryDir dir("escape-extract");
const fs::path wheel = dir.path() / "evil-1.0-py3-none-any.whl";
{
Zipper zipper(wheel.string(), Zipper::NO_COMPRESSION);
zipper.add_entry("pkg/module.py", "print('ok')\n", 12);
zipper.add_entry("../escape.txt", "escaped\n", 8);
zipper.finalize();
}
std::string error;
CHECK_FALSE(extract_zip_to_directory(wheel, dir.path() / "out", error));
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("escapes the plugin package"));
CHECK_FALSE(fs::exists(dir.path() / "escape.txt"));
}