Compare commits

..
Author SHA1 Message Date
Hanif Koh f307785cad Drop Cached Bytecode Through the Interpreter in the Module Release Test 2026-10-16 19:18:02 +08:00
Hanif Koh 6bc835ea30 Keep Python Bytecode out of the Bundled Python Home
The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.
2026-10-16 16:23:51 +08:00
42 changed files with 258 additions and 1154 deletions

No files matched your search

@@ -574,8 +574,7 @@ DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the
paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44, paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
`WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware `WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On a hypervisor guest (`/proc/cpuinfo` `hypervisor` flag) and WebKitGTK >= 2.44 it sets DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` alone, since virtual GPUs cannot back the hardware path either. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked is set, and sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) inside that branch when the linked
WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case WebKitGTK is older than 2.42: a forced X11 backend in a Wayland session is the only default-path case
that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native that runs through XWayland, which `DISPLAY` and `WAYLAND_DISPLAY` alone do not tell you (a native
@@ -568,18 +568,13 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
sets the DMABUF workarounds and then branches on `GDK_BACKEND`: sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK - **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check. Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Hypervisor guests**, same position: when `is_hypervisor_guest()` (the `hypervisor` flag in
`/proc/cpuinfo`) and WebKitGTK >= 2.44, it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` non-replacing.
Virtual GPUs (VirtualBox VMSVGA, VMware, QEMU) cannot back the hardware DMABUF path either, and
FORCE_SHM alone was confirmed to fix web views in VirtualBox; the `WEBKIT_FORCE_DMABUF_RENDERER` pair
is not needed without the Debian `isNVIDIA()` patch. The flag is x86-only, so ARM guests are not covered.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and - **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets `WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`, and in that same branch it sets
`WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42 `WEBKIT_DISABLE_COMPOSITING_MODE=1` non-replacing when the linked WebKitGTK predates 2.42
@@ -595,9 +590,9 @@ Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before - **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's `WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
hardware DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate, skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
-13
View File
@@ -288,19 +288,6 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts. # 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") 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}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_INSTALL_RPATH "$ORIGIN")
-50
View File
@@ -111,54 +111,6 @@ else
export CMAKE_POLICY_COMPAT="" export CMAKE_POLICY_COMPAT=""
fi 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 "Build params:"
echo " - ARCH: $ARCH" echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG" echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -210,7 +162,6 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target deps cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -313,7 +264,6 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \ -DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \ -DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} ${CMAKE_POLICY_COMPAT}
fi fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET" cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
+2 -5
View File
@@ -350,11 +350,8 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
while no agent is live. Whole documents are Bambu's business alone: while no agent is live. Whole documents are Bambu's business alone:
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under `BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not `tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's: none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
file degrades to identity with a log line rather than failing. file degrades to identity with a log line rather than failing.
| Boundary | Where it translates | | Boundary | Where it translates |
-16
View File
@@ -1126,19 +1126,6 @@ static void load_downward_settings_list_from_config(std::string config_file, std
} }
} }
#ifdef __WXGTK__
// Virtual GPUs (VirtualBox, VMware, QEMU) cannot back WebKit's hardware DMABUF renderer.
static bool is_hypervisor_guest()
{
std::ifstream cpuinfo("/proc/cpuinfo");
std::string line;
while (std::getline(cpuinfo, line))
if (boost::starts_with(line, "flags"))
return line.find(" hypervisor") != std::string::npos;
return false;
}
#endif
int CLI::run(int argc, char **argv) int CLI::run(int argc, char **argv)
{ {
// Mark the main thread for the debugger and for runtime checks. // Mark the main thread for the debugger and for runtime checks.
@@ -1189,9 +1176,6 @@ int CLI::run(int argc, char **argv)
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false); ::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
} }
if (is_hypervisor_guest() && WebView::SupportsForceSharedMemory())
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
if (x11_opt_in) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
+7 -3
View File
@@ -812,8 +812,7 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole. * 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. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
* *
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall * Odd walls should always go after their enclosing wall polygons.
* 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. * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/ */
@@ -887,7 +886,12 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) 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 continue; // points are too far away from each other
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) { 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) {
order_requirements.emplace(std::make_pair(nearby, here)); order_requirements.emplace(std::make_pair(nearby, here));
} else { } else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
-10
View File
@@ -3985,16 +3985,6 @@ 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_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("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))); 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 // Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params(); update_placeholder_parser_with_variant_params();
+71 -47
View File
@@ -120,13 +120,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@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 = { const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -153,13 +147,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@", "@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@", "@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@", "@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@"
}; };
@@ -1276,22 +1264,19 @@ void GCodeProcessor::run_post_process()
return ret; return ret;
}; };
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export) // Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
auto process_inline_placeholders = [&](std::string& gcode_line) { auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false; bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace. // Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos) if (gcode_line.find('@') == std::string::npos)
return processed; return processed;
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) { const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& placeholder = reserved_tag(tag); const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
size_t pos = gcode_line.find(placeholder); const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
while (pos != std::string::npos) { const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
gcode_line.replace(pos, placeholder.length(), value); const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
processed = true; const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
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; double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0) if (print_time_total_sec < 0.0)
@@ -1304,31 +1289,70 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60; int print_time_sec = day_remainder_seconds % 60;
double total_filament_mm = 0.0; // Replace print_time_total_sec
for (const auto& mm : filament_mm) { size_t pos = gcode_line.find(print_time_total_placeholder);
total_filament_mm += mm; 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_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2)); // Replace print_time_day
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day)); pos = gcode_line.find(print_time_day_placeholder);
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour)); while (pos != std::string::npos) {
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute)); char buf[64];
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec)); sprintf(buf, "%d", print_time_day);
replace_tag(ETags::Used_Filament_Length_Placeholder, gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m processed = true;
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
// 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() ? // Replace print_time_hour
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) : pos = gcode_line.find(print_time_hour_placeholder);
short_time("N/A")); while (pos != std::string::npos) {
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0; char buf[64];
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2)); sprintf(buf, "%d", print_time_hour);
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2)); gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder, processed = true;
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2)); pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder, }
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
// 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));
}
return processed; return processed;
}; };
-6
View File
@@ -601,12 +601,6 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder, Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder, Print_Time_Sec_Placeholder,
Used_Filament_Length_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)]; } 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,35 +211,6 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
return decode_colored_png(stream, 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. // 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/ // Based on https://www.lemoda.net/c/write-png/
-3
View File
@@ -50,9 +50,6 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
bool is_png(const ReadBuf &pngbuf); 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 { struct ReadBufStream: public IStream {
const ReadBuf &rbuf_ref; const ReadBuf &rbuf_ref;
size_t pos = 0; size_t pos = 0;
+24 -65
View File
@@ -62,12 +62,7 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole. // Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth; unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them. // 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; 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). // Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children; std::vector<PerimeterGeneratorLoop> children;
@@ -577,8 +572,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop)); 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. // 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; 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()); std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
} }
else { else {
@@ -1696,24 +1690,13 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets); last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a // ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
if (i == 0 && ! holes[0].empty()) { if (i == 0 && ! holes[0].empty()) {
auto reach = [&last](double room) { const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON))); const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
}; for (PerimeterGeneratorLoop &hole : holes[0])
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) { hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
return intersection_pl(Polylines{ hole.split_at_first_point() }, ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
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 //BBS: refer to superslicer
@@ -1852,16 +1835,18 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that // if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim // 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; bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first, if (is_outer_wall_first ||
// then the contour, then the gap fill between them, as Arachne does. //BBS: always print outer wall first when there indeed has brim.
const PerimeterGeneratorLoops &island_loops = contours.front(); // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 && (this->layer_id == 0 &&
island_loops.front().children.front().is_narrow_wall_hole; (this->object_config->brim_type == BrimType::btOuterOnly ||
if (is_outer_wall_first || this->outer_walls_first_for_brim()) this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
entities.reverse(); entities.reverse();
// Orca: sandwich mode. Apply after 1st layer. // Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){ else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
entities.reverse(); // reverse all entities - order them from external to internal 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 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. int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2427,25 +2412,11 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index // Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) { auto reorderFromReference = [&](int referenceIndex) {
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared std::unordered_set<int> firstLevelIndices;
// by the outer walls on both sides, so those join this island and are printed before it too, holes first. firstLevelIndices.insert(referenceIndex);
std::vector<int> outerIndices{referenceIndex};
std::vector<int> firstLevelTouchingIndices; // Find first level touching perimeters
for (size_t next = 0; next < outerIndices.size(); ++next) std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
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 // Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter // The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up // hence printing it immediately after the external perimeter should speed things up
@@ -2456,10 +2427,8 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
} }
// Insert first level perimeters into reordered list // Insert first level perimeters into reordered list
for (int idx : outerIndices) { reordered.push_back(entities[referenceIndex]);
reordered.push_back(entities[idx]); includedIndices.insert(referenceIndex);
includedIndices.insert(idx);
}
for (int idx : firstLevelTouchingIndices) { for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) { if (includedIndices.count(idx) == 0) {
@@ -2788,7 +2757,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first = is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim(); this->config->wall_sequence == WallSequence::OuterInner;
} }
if (is_outer_wall_first) { if (is_outer_wall_first) {
start_perimeter = 0; start_perimeter = 0;
@@ -3073,16 +3042,6 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices; 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) 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); float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
-6
View File
@@ -164,8 +164,6 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } 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. // 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; 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: private:
std::vector<Polygons> generate_lower_polygons_series(float width); std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -185,10 +183,6 @@ private:
Polygons m_lower_slices_polygons; 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 #endif
-1
View File
@@ -2293,7 +2293,6 @@ 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; BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue; 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.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config)); //preset->config.apply(std::move(config));
Preset::normalize(preset->config); Preset::normalize(preset->config);
+9 -8
View File
@@ -17,7 +17,6 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "Layer.hpp" #include "Layer.hpp"
#include "MutablePolygon.hpp" #include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp" #include "SLA/IndexedMesh.hpp"
@@ -1465,13 +1464,15 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") { || opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt. // Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial); steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type" || opt_key == "brim_width") { if (opt_key == "brim_type") {
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first. const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
auto outer_walls_first = [](const ConfigOptionResolver &config) { const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value, //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
config.option<ConfigOptionFloat>("brim_width")->value); //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
if (outer_walls_first(old_config) != outer_walls_first(new_config)) // 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)
steps.emplace_back(posPerimeters); steps.emplace_back(posPerimeters);
} }
} else if ( } else if (
+8 -5
View File
@@ -707,10 +707,8 @@ void tree_supports_generate_paths(
false); false);
} }
if (eec) { if (eec) {
if (!eec->empty()) { std::reverse(eec->entities.begin(), eec->entities.end());
std::reverse(eec->entities.begin(), eec->entities.end()); dst.emplace_back(eec.release());
dst.emplace_back(eec.release());
}
} }
} }
} }
@@ -1827,7 +1825,12 @@ void generate_support_toolpaths(
no_sort = true; no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic && } else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) { (config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params); // 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);
done = true; done = true;
} }
if (! done) if (! done)
+22 -10
View File
@@ -2149,9 +2149,7 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning; const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width; coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value; 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 // 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 // tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed: // support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2203,7 +2201,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0; size_t first_base_roof_area = 0;
bool floor_interface_as_base = false; bool floor_interface_as_base = false;
bool has_circle_node = false; bool has_circle_node = false;
ExPolygons extra_wall_areas; bool need_extra_wall = false;
ExPolygons collision_sharp_tails; ExPolygons collision_sharp_tails;
ExPolygons collision_base; ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{ auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2281,6 +2279,7 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0)); // area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true; if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface // merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted. // Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2295,10 +2294,6 @@ 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) if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area); append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 && else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2535,8 +2530,7 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue; //if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base); area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly); area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = !area_groups.back().need_infill && area_groups.back().need_extra_wall = 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) { for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx]; auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3439,6 +3433,11 @@ 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++) { for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr]; std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue; if (curr_layer_nodes.empty()) continue;
@@ -3448,17 +3447,20 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii; std::vector<double> radii;
std::vector<SupportNode *> branch; std::vector<SupportNode *> branch;
SupportNode * p_node = node; SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403 // 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 // 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) { if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position); pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius); radii.push_back(p_node->child->radius);
branch.push_back(p_node->child); branch.push_back(p_node->child);
total_height += p_node->child->height;
} }
do { do {
pts.push_back(p_node->position); pts.push_back(p_node->position);
radii.push_back(p_node->radius); radii.push_back(p_node->radius);
branch.push_back(p_node); branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent; p_node = p_node->parent;
} while (p_node && !p_node->is_processed); } while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue; if (pts.size() < 3) continue;
@@ -3477,6 +3479,9 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i]; branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2; branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true; 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; BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
} }
} }
@@ -3484,6 +3489,13 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1); std::swap(pts, pts1);
std::swap(radii, radii1); 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,6 +135,7 @@ struct SupportNode
TreeNodeType type = eCircle; TreeNodeType type = eCircle;
bool is_corner = false; bool is_corner = false;
bool is_processed = false; bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false; bool is_sharp_tail = false;
bool valid = true; bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+8 -10
View File
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color(); return decode_height_texture(layer).has_color();
} }
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded) DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{ {
DecodedHeightTexture result; DecodedHeightTexture result;
if (layer.empty()) if (layer.empty())
@@ -351,15 +351,13 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
} }
} }
if (keep_decoded) { std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex); // Opportunistically drop entries for image_data that no longer exists anywhere, so the
// 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.
// cache doesn't grow without bound across many add/remove-texture cycles in a long session. auto &entries = g_decoded_texture_cache.entries;
auto &entries = g_decoded_texture_cache.entries; for (auto it = entries.begin(); it != entries.end();)
for (auto it = entries.begin(); it != entries.end();) it = it->second.first.expired() ? entries.erase(it) : std::next(it);
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 };
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, // The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
+2 -3
View File
@@ -599,9 +599,8 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
// colour PNGs are accepted; for a colour source the height is its luminance, using the same // 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 // 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 // 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. keep_decoded = false leaves the // DecodedHeightTexture if image_data is empty or is not a PNG at all.
// 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);
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 // 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. // texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
-7
View File
@@ -842,13 +842,6 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
bool check_layer_id_pattern(const std::string& pattern, int layer_id); 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 } // namespace Slic3r
#if WIN32 #if WIN32
+8 -53
View File
@@ -17,7 +17,6 @@
#if defined(_MSC_VER) || defined(__MINGW64__) #if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp" #include "boost/nowide/cstdio.hpp"
#include "boost/nowide/convert.hpp"
#endif #endif
#include "I18N.hpp" #include "I18N.hpp"
@@ -63,11 +62,7 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
FILE *f = nullptr; FILE *f = nullptr;
#if defined(_MSC_VER) || defined(__MINGW64__) #if defined(_MSC_VER) || defined(__MINGW64__)
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an f = boost::nowide::fopen(fname, mode);
// 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) #elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
f = fopen64(fname, mode); f = fopen64(fname, mode);
#else #else
@@ -106,14 +101,6 @@ bool close_zip(mz_zip_archive *zip, bool isread)
} }
return ret; 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) bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
@@ -129,36 +116,6 @@ 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_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); } 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) std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
{ {
if (stat.m_is_utf8) if (stat.m_is_utf8)
@@ -199,13 +156,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
} }
const std::string dest_file = dest_dir + "/" + stat.m_filename; const std::string dest_file = dest_dir + "/" + stat.m_filename;
try { try {
// 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);
const boost::filesystem::path io_dest = extended_path(dest_path);
if (stat.m_is_directory) { if (stat.m_is_directory) {
if (!boost::filesystem::exists(io_dest)) const boost::filesystem::path dest_path(dest_file);
boost::filesystem::create_directories(io_dest); if (!boost::filesystem::exists(dest_path))
boost::filesystem::create_directories(dest_path);
continue; continue;
} }
if (stat.m_uncomp_size == 0) { if (stat.m_uncomp_size == 0) {
@@ -213,9 +167,10 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
continue; continue;
} }
// Replace a symlink at the destination rather than writing through it. // Replace a symlink at the destination rather than writing through it.
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest))) const boost::filesystem::path dest_path(dest_file);
boost::filesystem::remove(io_dest); if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) { boost::filesystem::remove(dest_path);
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed"; BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
close_zip_reader(&archive); close_zip_reader(&archive);
return false; return false;
-5
View File
@@ -1,7 +1,6 @@
#ifndef MINIZ_EXTENSION_HPP #ifndef MINIZ_EXTENSION_HPP
#define MINIZ_EXTENSION_HPP #define MINIZ_EXTENSION_HPP
#include <boost/filesystem/path.hpp>
#include <string> #include <string>
#include <miniz.h> #include <miniz.h>
@@ -14,10 +13,6 @@ 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); 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. // 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); 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 { class MZ_Archive {
public: public:
-40
View File
@@ -2254,45 +2254,5 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
return mod >= 0 && mod < count; 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 }; // namespace Slic3r
+1 -1
View File
@@ -75,7 +75,7 @@ std::vector<float> Layers::get_zs() const
size_t Layers::get_layer_id_at(float z) const size_t Layers::get_layer_id_at(float z) const
{ {
auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z > z; }); auto iter = std::upper_bound(m_items.begin(), m_items.end(), z, [](float z, const Item& item) { return item.z < z; });
return std::distance(m_items.begin(), iter); return std::distance(m_items.begin(), iter);
} }
@@ -174,10 +174,6 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048;
// The full-size image is only ever adjusted once, on commit. // The full-size image is only ever adjusted once, on commit.
constexpr int ADJUST_PREVIEW_MAX_PX = 384; 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 // 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 // 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. // cost growing to matter: it is a ring around a perimeter, not an area.
@@ -4122,7 +4118,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge
if (entry.image_data) { if (entry.image_data) {
TextureDisplacementLayer probe; TextureDisplacementLayer probe;
probe.image_data = entry.image_data; probe.image_data = entry.image_data;
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false)); entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe));
if (!entry.thumbnail) if (!entry.thumbnail)
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
} else { } else {
@@ -4335,8 +4331,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
std::optional<TextureLibraryEntry> to_remove; std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false; bool import_clicked = false;
bool download_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 // 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 // overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4354,30 +4348,11 @@ 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 // 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 // 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 tiles it laid out, drawn or not. // carries a remove button on hover. Returns how many it drew.
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) { const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
int n = 0; int n = 0;
for (const TextureLibraryEntry *entry : entries) { for (const TextureLibraryEntry *entry : entries) {
const ImVec2 rect_min = tile_pos(origin, n++); ImGui::SetCursorScreenPos(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()); ImGui::PushID(entry->path.c_str());
const LibraryTexture *tex = get_library_texture(entry->path); const LibraryTexture *tex = get_library_texture(entry->path);
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr; GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
@@ -4400,6 +4375,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (removable) if (removable)
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax(); const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
const bool current = entry->path == layer->path;
const bool hovered = ImGui::IsItemHovered(); const bool hovered = ImGui::IsItemHovered();
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner, // 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. // fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
@@ -4408,6 +4384,9 @@ 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), 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); 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) if (hovered)
panel_tooltip(entry->name); panel_tooltip(entry->name);
@@ -4596,8 +4575,6 @@ 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::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(); 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. // Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false; m_picker_reveal_selected = false;
ImGui::EndChild(); ImGui::EndChild();
+1 -6
View File
@@ -21178,12 +21178,7 @@ void Plater::print_job_finished(wxCommandEvent &evt)
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return; if (!dev) return;
const std::string dev_id = evt.GetString().ToStdString(); dev->set_selected_machine(evt.GetString().ToStdString());
MachineObject* selected = dev->get_selected_machine();
// After a LAN print, the completion event reselects the same printer.
// Keep the active session while asynchronous status traffic may still be in flight.
if (!selected || selected->get_dev_id() != dev_id || selected->connection_type() != "lan")
dev->set_selected_machine(dev_id);
p->main_frame->request_select_tab(TAB_ID_MONITOR); p->main_frame->request_select_tab(TAB_ID_MONITOR);
// Selects the status page on a built Device tab; one built by the switch starts there. // Selects the status page on a built Device tab; one built by the switch starts there.
MonitorPanel* curr_monitor = MonitorPanel::if_built(); MonitorPanel* curr_monitor = MonitorPanel::if_built();
+17 -29
View File
@@ -474,14 +474,17 @@ void Selection::add_curr_plate()
if (!m_valid) if (!m_valid)
return; 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!")); wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!"));
m_mode = Instance; m_mode = Instance;
clear(); clear();
do_add_volumes(volume_idxs);
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(); update_type();
this->set_bounding_boxes_dirty(); this->set_bounding_boxes_dirty();
@@ -508,14 +511,20 @@ void Selection::remove_curr_plate()
if (!m_valid) if (!m_valid)
return; return;
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index()); PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
if (volume_idxs.empty()) if (plate->empty())
return; return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All")); wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All"));
m_mode = Instance; m_mode = Instance;
clear(); clear();
do_add_volumes(volume_idxs);
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(); update_type();
this->set_bounding_boxes_dirty(); this->set_bounding_boxes_dirty();
@@ -2261,27 +2270,6 @@ std::vector<unsigned int> Selection::get_volume_idxs_from_volume(unsigned int ob
return idxs; 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> Selection::get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const
{ {
std::vector<unsigned int> idxs; std::vector<unsigned int> idxs;
-2
View File
@@ -409,8 +409,6 @@ public:
std::vector<unsigned int> get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const; 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 // 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; 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 // 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; 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 // returns the list of idxs of the volumes contained in the given list but not in the selection
+15 -3
View File
@@ -223,11 +223,23 @@ 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>()); return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
} }
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is. // True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
// Anything else is converted on load. // 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.
bool is_supported_height_map(const std::vector<unsigned char> &bytes) bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{ {
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() }); 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);
} }
std::vector<TextureLibraryEntry> g_library; std::vector<TextureLibraryEntry> g_library;
+4 -11
View File
@@ -7,8 +7,6 @@
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevManager.h"
#include <algorithm>
#include <array>
#include <boost/format.hpp> #include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
@@ -18,7 +16,6 @@
#include <cmath> #include <cmath>
#include <slic3r/GUI/DeviceManager.hpp> #include <slic3r/GUI/DeviceManager.hpp>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
#include <vector> #include <vector>
using json = nlohmann::json; using json = nlohmann::json;
@@ -67,16 +64,12 @@ const BambuFilamentIdMap& bambu_filament_id_map()
return map; return map;
} }
// Keys whose string value is a filament id. "cate" and "fila_id" are the H2C nozzle rack's: the // Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place.
// auto nozzle mapping request and the rack status in the push message.
constexpr std::array<std::string_view, 5> filament_id_keys{"tray_info_idx", "filament_id", "filamentId", "cate", "fila_id"};
// Rewrites every string under a filament id key, at any depth, in place.
void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map) void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map)
{ {
if (j.is_object()) { if (j.is_object()) {
for (auto& [key, value] : j.items()) { for (auto& [key, value] : j.items()) {
if (value.is_string() && std::find(filament_id_keys.begin(), filament_id_keys.end(), key) != filament_id_keys.end()) { if (value.is_string() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) {
auto it = map.find(value.get_ref<const std::string&>()); auto it = map.find(value.get_ref<const std::string&>());
if (it != map.end()) if (it != map.end())
value = it->second; value = it->second;
@@ -91,8 +84,8 @@ void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::st
// Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical. // Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical.
std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map) std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map)
{ {
if (map.empty() || std::none_of(filament_id_keys.begin(), filament_id_keys.end(), if (map.empty() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos &&
[&text](std::string_view key) { return text.find(key) != std::string::npos; })) text.find("filamentId") == std::string::npos))
return text; // nothing to map, skip the parse (moved, not copied) return text; // nothing to map, skip the parse (moved, not copied)
try { try {
json j = json::parse(text); json j = json::parse(text);
+17 -6
View File
@@ -34,6 +34,10 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#ifdef WIN32
#include <boost/locale/encoding_utf.hpp>
#endif
namespace Slic3r { namespace Slic3r {
const char* const INSTALL_STATE_FILE = ".install_state.json"; const char* const INSTALL_STATE_FILE = ".install_state.json";
@@ -661,7 +665,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
boost::system::error_code ec; boost::system::error_code ec;
fs::create_directories(extended_path(destination), ec); fs::create_directories(destination, ec);
if (ec) { if (ec) {
error = "Failed to create plugin staging directory: " + ec.message(); error = "Failed to create plugin staging directory: " + ec.message();
return false; return false;
@@ -705,7 +709,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
const fs::path output_path = destination / relative_path; 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)) { if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
fs::create_directories(extended_path(output_path), ec); fs::create_directories(output_path, ec);
if (ec) { if (ec) {
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message(); error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
return false; return false;
@@ -713,19 +717,26 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
continue; continue;
} }
fs::create_directories(extended_path(output_path.parent_path()), ec); fs::create_directories(output_path.parent_path(), ec);
if (ec) { if (ec) {
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message(); error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
return false; return false;
} }
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) { if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
error = "Plugin zip file conflicts with an existing directory: " + output_path.string(); error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
return false; return false;
} }
// Shared with the rest of the archive code: long-path capable and binary-safe. const std::string encoded_output_path = encode_path(output_path.string().c_str());
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) { 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) {
error = "Failed to extract plugin zip entry " + relative_key + ": " + error = "Failed to extract plugin zip entry " + relative_key + ": " +
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive)); MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false; return false;
+13
View File
@@ -584,6 +584,19 @@ bool PythonInterpreter::initialize()
} }
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully"; BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
// Keep bytecode out of the Python home. The shipped .pyc files are stale against their
// sources, so a writable install would rewrite them inside the app (breaking a signed
// macOS bundle's seal) and a read-only one would recompile them on every launch.
const std::string pycache_prefix = data_dir() + "/python/pycache";
#ifdef _WIN32
status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
#else
status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
#endif
if (PyStatus_Exception(status))
BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
<< (status.err_msg ? status.err_msg : "unknown error");
// Use pybind11's scoped_interpreter which properly initializes // Use pybind11's scoped_interpreter which properly initializes
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig // pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
// does not set up thread state tracking that pybind11 requires. // does not set up thread state tracking that pybind11 requires.
-382
View File
@@ -22,7 +22,6 @@
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp" #include "libslic3r/Polyline.hpp"
#include <limits> #include <limits>
#include <string> #include <string>
@@ -995,384 +994,3 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime
CHECK(layers[i].holes_ccw.front() == ccw); 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,65 +735,6 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
CHECK(per_layer.find(";Layer:399 ") != std::string::npos); 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]") TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{ {
Print print; Print print;
-50
View File
@@ -18,7 +18,6 @@
#include <fstream> #include <fstream>
#include <ios> #include <ios>
#include <iterator> #include <iterator>
#include <libslic3r/Utils.hpp>
#include <miniz.h> #include <miniz.h>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -177,55 +176,6 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"}); 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 #ifndef _WIN32
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]") TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
{ {
@@ -985,37 +985,6 @@ 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]") TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
{ {
ScopedTemporaryDir temp_dir; ScopedTemporaryDir temp_dir;
-46
View File
@@ -521,49 +521,3 @@ TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]")
CHECK_FALSE(ascii_iequals("[", "{")); CHECK_FALSE(ascii_iequals("[", "{"));
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
}
@@ -81,24 +81,3 @@ TEST_CASE("Payload rewrite covers nested trays, calibration lists and mapping in
CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json"); CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json");
CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})"); CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})");
} }
TEST_CASE("Payload rewrite covers the nozzle rack status and the auto nozzle mapping request", "[BambuFilamentIds]")
{
const ScopedRepoResourcesDir repo_resources;
const std::string petg_hf = orca_id_of("GFG96");
const std::string rack_status =
R"({"print":{"device":{"nozzle":{"info":[{"id":16,"fila_id":"GFG96","color_m":"FFFFFFFF"},{"id":17,"fila_id":"P22f54c9"}]}}}})";
json inbound = json::parse(BBLPrinterAgent::to_orca_payload(rack_status));
CHECK(inbound["print"]["device"]["nozzle"]["info"][0]["fila_id"] == petg_hf);
CHECK(inbound["print"]["device"]["nozzle"]["info"][1]["fila_id"] == "P22f54c9"); // user id: untouched
const std::string request = R"({"print":{"command":"get_auto_nozzle_mapping","ams_mapping":[65535,65024],)"
R"("fila_info":[{"id":2,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"}],)"
R"("nozzle_info":[{"pos":16,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"},{"pos":1,"cate":""}]}})";
json outbound = json::parse(BBLPrinterAgent::from_orca_payload(request));
CHECK(outbound["print"]["fila_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][1]["cate"] == ""); // empty nozzle
CHECK(outbound["print"]["ams_mapping"] == json::parse("[65535,65024]"));
}
-69
View File
@@ -3,7 +3,6 @@
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <ios> #include <ios>
#include <libslic3r/Utils.hpp>
#include <slic3r/plugin/PluginLoader.hpp> #include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp> #include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp> #include <slic3r/plugin/PluginFsUtils.hpp>
@@ -11,17 +10,13 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp> #include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp> #include <catch2/matchers/catch_matchers_string.hpp>
#include "catch2/catch_message.hpp"
#include "plugin_test_utils.hpp" #include "plugin_test_utils.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <libslic3r/Zipper.hpp>
using namespace Slic3r; using namespace Slic3r;
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
@@ -36,14 +31,6 @@ fs::path write_py_file(const fs::path& dir, const std::string& filename, const s
return p; 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 } // namespace
TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]") TEST_CASE("install_plugin rejects a cloud UUID containing path traversal", "[PluginInstall]")
@@ -197,59 +184,3 @@ TEST_CASE("an unsubscribed cloud plugin follows its header version", "[PluginIns
REQUIRE(read_install_state(plugin_dir, scanned)); REQUIRE(read_install_state(plugin_dir, scanned));
CHECK(scanned.installed_version == "1.0.0"); 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"));
}
+16 -2
View File
@@ -615,8 +615,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path",
// Ensure the next import executes the new submodule rather than reusing a stale package child. // Ensure the next import executes the new submodule rather than reusing a stale package child.
write_package("'new'"); write_package("'new'");
boost::system::error_code ec; {
fs::remove_all(package_root / "__pycache__", ec); // The rewrite can keep the old mtime and size, so drop the bytecode wherever the
// interpreter caches it.
PythonGILState gil;
REQUIRE(gil);
PyObjectPtr util(PyImport_ImportModule("importlib.util"));
REQUIRE(util);
for (const char* file : {"__init__.py", "sub.py"}) {
PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str()));
REQUIRE(source);
PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get()));
REQUIRE(cached);
boost::system::error_code ec;
fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec);
}
}
paths.clear(); paths.clear();
modules.clear(); modules.clear();