mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-18 21:20:57 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df80c9ae22 | ||
|
|
1ee0b730a1 | ||
|
|
72ba7a2b35 | ||
|
|
8160f64cae | ||
|
|
fd28f6c189 | ||
|
|
9b2fcaa53a | ||
|
|
0acf1e6680 | ||
|
|
c6804fbb68 | ||
|
|
6d40ec93f6 | ||
|
|
82aee1abdd | ||
|
|
cefdd9b609 | ||
|
|
caade80624 | ||
|
|
cbd8ce98d5 | ||
|
|
99714afc4c | ||
|
|
3d07663ab2 | ||
|
|
4149401c53 |
No files matched your search
@@ -574,7 +574,8 @@ 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 the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
|
DMABUF renderer. On a hypervisor guest (`/proc/cpuinfo` `hypervisor` flag) and WebKitGTK >= 2.44 it sets
|
||||||
|
`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,13 +568,18 @@ 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 NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
|
sets the DMABUF workarounds 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
|
||||||
@@ -590,9 +595,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 reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
- **Rule:** On NVIDIA and hypervisor guests reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
|
||||||
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
|
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver and virtual GPUs cannot back WebKit's
|
||||||
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
|
hardware 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
|
||||||
|
|||||||
@@ -288,6 +288,19 @@ 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")
|
||||||
|
|||||||
@@ -111,6 +111,54 @@ 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"
|
||||||
@@ -162,6 +210,7 @@ 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
|
||||||
@@ -264,6 +313,7 @@ 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"
|
||||||
|
|||||||
@@ -350,8 +350,11 @@ 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` or `filamentId` at any depth; text that does not parse, or carries
|
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
|
||||||
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
|
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
|
||||||
|
`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 |
|
||||||
|
|||||||
@@ -1126,6 +1126,19 @@ 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.
|
||||||
@@ -1176,6 +1189,9 @@ 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.";
|
||||||
|
|||||||
@@ -812,7 +812,8 @@ 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 should always go after their enclosing wall polygons.
|
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
|
||||||
|
* like any other when printing inner to outer.
|
||||||
*
|
*
|
||||||
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
|
||||||
*/
|
*/
|
||||||
@@ -886,12 +887,7 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
|
|||||||
continue; // not directly adjacent
|
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 (here->is_odd || nearby->is_odd) {
|
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
|
||||||
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);
|
||||||
|
|||||||
@@ -3985,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
this->placeholder_parser().set("print_time_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();
|
||||||
|
|||||||
@@ -120,7 +120,13 @@ 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 = {
|
||||||
@@ -147,7 +153,13 @@ 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@"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process()
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
|
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
|
||||||
auto process_inline_placeholders = [&](std::string& gcode_line) {
|
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;
|
||||||
|
|
||||||
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
|
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
|
||||||
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
|
const std::string& placeholder = reserved_tag(tag);
|
||||||
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
|
size_t pos = gcode_line.find(placeholder);
|
||||||
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
|
while (pos != std::string::npos) {
|
||||||
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
|
gcode_line.replace(pos, placeholder.length(), value);
|
||||||
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
|
processed = true;
|
||||||
|
pos = gcode_line.find(placeholder, pos + value.length());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
|
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)
|
||||||
@@ -1289,70 +1304,31 @@ 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;
|
||||||
|
|
||||||
// Replace print_time_total_sec
|
double total_filament_mm = 0.0;
|
||||||
size_t pos = gcode_line.find(print_time_total_placeholder);
|
for (const auto& mm : filament_mm) {
|
||||||
while (pos != std::string::npos) {
|
total_filament_mm += mm;
|
||||||
char buf[64];
|
|
||||||
sprintf(buf, "%.2f", print_time_total_sec);
|
|
||||||
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
|
|
||||||
processed = true;
|
|
||||||
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace print_time_day
|
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
|
||||||
pos = gcode_line.find(print_time_day_placeholder);
|
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
|
||||||
while (pos != std::string::npos) {
|
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
|
||||||
char buf[64];
|
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
|
||||||
sprintf(buf, "%d", print_time_day);
|
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
|
||||||
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
|
replace_tag(ETags::Used_Filament_Length_Placeholder,
|
||||||
processed = true;
|
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
|
||||||
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
|
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
|
||||||
}
|
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
|
||||||
|
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
|
||||||
// Replace print_time_hour
|
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
|
||||||
pos = gcode_line.find(print_time_hour_placeholder);
|
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
|
||||||
while (pos != std::string::npos) {
|
short_time("N/A"));
|
||||||
char buf[64];
|
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
|
||||||
sprintf(buf, "%d", print_time_hour);
|
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
|
||||||
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
|
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
|
||||||
processed = true;
|
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
|
||||||
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
|
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
|
||||||
}
|
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
|
||||||
|
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
|
||||||
// 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -601,6 +601,12 @@ 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)]; }
|
||||||
|
|||||||
@@ -211,6 +211,35 @@ 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/
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ 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;
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ public:
|
|||||||
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
|
// 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;
|
||||||
|
|
||||||
@@ -572,7 +577,8 @@ 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 {
|
||||||
@@ -1690,13 +1696,24 @@ 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.
|
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
|
||||||
|
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
|
||||||
if (i == 0 && ! holes[0].empty()) {
|
if (i == 0 && ! holes[0].empty()) {
|
||||||
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
auto reach = [&last](double room) {
|
||||||
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
|
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
|
||||||
for (PerimeterGeneratorLoop &hole : holes[0])
|
};
|
||||||
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
|
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
|
||||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
|
return intersection_pl(Polylines{ hole.split_at_first_point() },
|
||||||
|
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
|
||||||
|
};
|
||||||
|
// Only Inner/Outer/Inner orders narrow wall holes apart.
|
||||||
|
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||||
|
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
|
||||||
|
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
|
||||||
|
for (PerimeterGeneratorLoop &hole : holes[0]) {
|
||||||
|
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
|
||||||
|
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//BBS: refer to superslicer
|
//BBS: refer to superslicer
|
||||||
@@ -1835,18 +1852,16 @@ 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;
|
||||||
if (is_outer_wall_first ||
|
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
|
||||||
//BBS: always print outer wall first when there indeed has brim.
|
// then the contour, then the gap fill between them, as Arachne does.
|
||||||
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
|
const PerimeterGeneratorLoops &island_loops = contours.front();
|
||||||
(this->layer_id == 0 &&
|
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
|
||||||
(this->object_config->brim_type == BrimType::btOuterOnly ||
|
island_loops.front().children.front().is_narrow_wall_hole;
|
||||||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
|
if (is_outer_wall_first || this->outer_walls_first_for_brim())
|
||||||
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){
|
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
|
||||||
entities.reverse(); // reverse all entities - order them from external to internal
|
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.
|
||||||
@@ -2412,11 +2427,25 @@ 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) {
|
||||||
std::unordered_set<int> firstLevelIndices;
|
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
|
||||||
firstLevelIndices.insert(referenceIndex);
|
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
|
||||||
|
std::vector<int> outerIndices{referenceIndex};
|
||||||
// Find first level touching perimeters
|
std::vector<int> firstLevelTouchingIndices;
|
||||||
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
|
for (size_t next = 0; next < outerIndices.size(); ++next)
|
||||||
|
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
|
||||||
|
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
|
||||||
|
continue;
|
||||||
|
firstLevelTouchingIndices.push_back(idx);
|
||||||
|
if (!entities[idx].extrusion->is_odd)
|
||||||
|
continue;
|
||||||
|
for (size_t i = 0; i < entities.size(); ++i)
|
||||||
|
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
|
||||||
|
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
|
||||||
|
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
|
||||||
|
outerIndices.push_back(int(i));
|
||||||
|
}
|
||||||
|
std::stable_partition(outerIndices.begin(), outerIndices.end(),
|
||||||
|
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
|
||||||
// Bring the largest first level perimeter to the front
|
// 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
|
||||||
@@ -2427,8 +2456,10 @@ 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
|
||||||
reordered.push_back(entities[referenceIndex]);
|
for (int idx : outerIndices) {
|
||||||
includedIndices.insert(referenceIndex);
|
reordered.push_back(entities[idx]);
|
||||||
|
includedIndices.insert(idx);
|
||||||
|
}
|
||||||
|
|
||||||
for (int idx : firstLevelTouchingIndices) {
|
for (int idx : firstLevelTouchingIndices) {
|
||||||
if (includedIndices.count(idx) == 0) {
|
if (includedIndices.count(idx) == 0) {
|
||||||
@@ -2757,7 +2788,7 @@ void PerimeterGenerator::process_arachne()
|
|||||||
|
|
||||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
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->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
|
||||||
}
|
}
|
||||||
if (is_outer_wall_first) {
|
if (is_outer_wall_first) {
|
||||||
start_perimeter = 0;
|
start_perimeter = 0;
|
||||||
@@ -3042,6 +3073,16 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
|
|||||||
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
|
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);
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ 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);
|
||||||
@@ -183,6 +185,10 @@ 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
|
||||||
@@ -2293,6 +2293,7 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
|
|||||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
|
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);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#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"
|
||||||
@@ -1464,15 +1465,13 @@ 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") {
|
if (opt_key == "brim_type" || opt_key == "brim_width") {
|
||||||
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
|
||||||
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
auto outer_walls_first = [](const ConfigOptionResolver &config) {
|
||||||
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
|
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
|
||||||
//to make the wall order of first layer to be outer-first
|
config.option<ConfigOptionFloat>("brim_width")->value);
|
||||||
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
|
};
|
||||||
// takes part in the same first-layer wall order rule.
|
if (outer_walls_first(old_config) != outer_walls_first(new_config))
|
||||||
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 (
|
||||||
|
|||||||
@@ -707,8 +707,10 @@ void tree_supports_generate_paths(
|
|||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
if (eec) {
|
if (eec) {
|
||||||
std::reverse(eec->entities.begin(), eec->entities.end());
|
if (!eec->empty()) {
|
||||||
dst.emplace_back(eec.release());
|
std::reverse(eec->entities.begin(), eec->entities.end());
|
||||||
|
dst.emplace_back(eec.release());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1825,12 +1827,7 @@ 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)) {
|
||||||
// Orca: A special case for the hollow Organic supports
|
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
|
||||||
// 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)
|
||||||
|
|||||||
@@ -2149,7 +2149,9 @@ 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:
|
||||||
@@ -2201,7 +2203,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;
|
||||||
bool need_extra_wall = false;
|
ExPolygons extra_wall_areas;
|
||||||
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 &{
|
||||||
@@ -2279,7 +2281,6 @@ 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.
|
||||||
@@ -2294,6 +2295,10 @@ void TreeSupport::draw_circles()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
|
||||||
|
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
|
||||||
|
append(extra_wall_areas, area);
|
||||||
|
|
||||||
if (obj_layer_nr>0 && node.distance_to_top < 0)
|
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 &&
|
||||||
@@ -2530,7 +2535,8 @@ 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 = need_extra_wall && !area_groups.back().need_infill;
|
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
|
||||||
|
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
|
||||||
}
|
}
|
||||||
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
|
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];
|
||||||
@@ -3433,11 +3439,6 @@ void TreeSupport::smooth_nodes()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float max_move = scale_(m_object_config->support_line_width / 2);
|
|
||||||
// if the branch is very tall, the tip also needs extra wall
|
|
||||||
float thresh_tall_branch = 100;
|
|
||||||
float thresh_dist_to_top = 30;
|
|
||||||
|
|
||||||
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
|
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;
|
||||||
@@ -3447,20 +3448,17 @@ 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;
|
||||||
@@ -3479,9 +3477,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3489,13 +3484,6 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ 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
|
||||||
|
|||||||
@@ -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)
|
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
|
||||||
{
|
{
|
||||||
DecodedHeightTexture result;
|
DecodedHeightTexture result;
|
||||||
if (layer.empty())
|
if (layer.empty())
|
||||||
@@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
if (keep_decoded) {
|
||||||
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
|
||||||
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
|
||||||
auto &entries = g_decoded_texture_cache.entries;
|
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
|
||||||
for (auto it = entries.begin(); it != entries.end();)
|
auto &entries = g_decoded_texture_cache.entries;
|
||||||
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
for (auto it = entries.begin(); it != entries.end();)
|
||||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
|
||||||
|
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
|
||||||
|
|||||||
@@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
|
|||||||
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
|
// 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.
|
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
|
||||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
|
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
|
||||||
|
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
|
||||||
|
|
||||||
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
|
// 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.
|
||||||
|
|||||||
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
|
|||||||
|
|
||||||
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
|
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
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#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"
|
||||||
@@ -62,7 +63,11 @@ 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__)
|
||||||
f = boost::nowide::fopen(fname, mode);
|
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
|
||||||
|
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
|
||||||
|
const std::wstring wide_path = boost::nowide::widen(fname);
|
||||||
|
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
|
||||||
|
f = boost::nowide::fopen(extended.c_str(), mode);
|
||||||
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
|
||||||
f = fopen64(fname, mode);
|
f = fopen64(fname, mode);
|
||||||
#else
|
#else
|
||||||
@@ -101,6 +106,14 @@ 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)
|
||||||
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
|
|||||||
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
|
bool close_zip_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)
|
||||||
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
|
|||||||
}
|
}
|
||||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
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) {
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
if (!boost::filesystem::exists(io_dest))
|
||||||
if (!boost::filesystem::exists(dest_path))
|
boost::filesystem::create_directories(io_dest);
|
||||||
boost::filesystem::create_directories(dest_path);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (stat.m_uncomp_size == 0) {
|
if (stat.m_uncomp_size == 0) {
|
||||||
@@ -167,10 +213,9 @@ 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.
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
|
||||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
boost::filesystem::remove(io_dest);
|
||||||
boost::filesystem::remove(dest_path);
|
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, 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;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#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>
|
||||||
|
|
||||||
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
|
|||||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
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:
|
||||||
|
|||||||
@@ -2254,5 +2254,45 @@ 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
|
||||||
@@ -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,6 +174,10 @@ 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.
|
||||||
@@ -4118,7 +4122,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));
|
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
|
||||||
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 {
|
||||||
@@ -4331,6 +4335,8 @@ 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
|
||||||
@@ -4348,11 +4354,30 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|||||||
};
|
};
|
||||||
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
|
// 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 it drew.
|
// carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
|
||||||
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
|
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) {
|
||||||
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
|
const ImVec2 rect_min = tile_pos(origin, n++);
|
||||||
|
const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile);
|
||||||
|
const bool current = entry->path == layer->path;
|
||||||
|
// The selected tile is centred even when it is off screen, so it is drawn and loaded next frame.
|
||||||
|
if (current && m_picker_reveal_selected) {
|
||||||
|
ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f);
|
||||||
|
m_parent.request_extra_frame();
|
||||||
|
}
|
||||||
|
if (!ImGui::IsRectVisible(rect_min, rect_max))
|
||||||
|
continue;
|
||||||
|
// Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames.
|
||||||
|
const bool cached = m_library_textures.count(entry->path) != 0;
|
||||||
|
if (!cached && loads_left == 0) {
|
||||||
|
loads_deferred = true;
|
||||||
|
dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!cached)
|
||||||
|
--loads_left;
|
||||||
|
ImGui::SetCursorScreenPos(rect_min);
|
||||||
ImGui::PushID(entry->path.c_str());
|
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;
|
||||||
@@ -4375,7 +4400,6 @@ 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.
|
||||||
@@ -4384,9 +4408,6 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|||||||
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
|
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);
|
||||||
|
|
||||||
@@ -4575,6 +4596,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|||||||
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
|
ImGui::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();
|
||||||
|
|||||||
@@ -21178,7 +21178,12 @@ 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;
|
||||||
|
|
||||||
dev->set_selected_machine(evt.GetString().ToStdString());
|
const std::string dev_id = 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();
|
||||||
|
|||||||
@@ -474,17 +474,14 @@ 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();
|
||||||
@@ -511,20 +508,14 @@ void Selection::remove_curr_plate()
|
|||||||
if (!m_valid)
|
if (!m_valid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
|
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
|
||||||
if (plate->empty())
|
if (volume_idxs.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();
|
||||||
@@ -2270,6 +2261,27 @@ 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;
|
||||||
|
|||||||
@@ -409,6 +409,8 @@ 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
|
||||||
|
|||||||
@@ -223,23 +223,11 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
|
|||||||
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
|
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
|
||||||
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
|
// Anything else is converted on load.
|
||||||
// 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)
|
||||||
{
|
{
|
||||||
if (bytes.empty())
|
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
|
||||||
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;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#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>
|
||||||
@@ -16,6 +18,7 @@
|
|||||||
#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;
|
||||||
@@ -64,12 +67,16 @@ const BambuFilamentIdMap& bambu_filament_id_map()
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place.
|
// Keys whose string value is a filament id. "cate" and "fila_id" are the H2C nozzle rack's: the
|
||||||
|
// 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() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) {
|
if (value.is_string() && std::find(filament_id_keys.begin(), filament_id_keys.end(), key) != filament_id_keys.end()) {
|
||||||
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;
|
||||||
@@ -84,8 +91,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() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos &&
|
if (map.empty() || std::none_of(filament_id_keys.begin(), filament_id_keys.end(),
|
||||||
text.find("filamentId") == std::string::npos))
|
[&text](std::string_view key) { return text.find(key) != 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);
|
||||||
|
|||||||
@@ -34,10 +34,6 @@
|
|||||||
#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";
|
||||||
@@ -665,7 +661,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(destination, ec);
|
fs::create_directories(extended_path(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;
|
||||||
@@ -709,7 +705,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
|||||||
|
|
||||||
const fs::path output_path = destination / relative_path;
|
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(output_path, ec);
|
fs::create_directories(extended_path(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;
|
||||||
@@ -717,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_directories(output_path.parent_path(), ec);
|
fs::create_directories(extended_path(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(output_path, ec) && fs::is_directory(output_path, ec)) {
|
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
|
||||||
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string encoded_output_path = encode_path(output_path.string().c_str());
|
// Shared with the rest of the archive code: long-path capable and binary-safe.
|
||||||
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
|
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
|
||||||
#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;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#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>
|
||||||
@@ -994,3 +995,384 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime
|
|||||||
CHECK(layers[i].holes_ccw.front() == ccw);
|
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);
|
||||||
|
}
|
||||||
@@ -735,6 +735,65 @@ 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;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#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>
|
||||||
@@ -176,6 +177,55 @@ TEST_CASE("Confined extraction writes nothing outside the target for Windows-spe
|
|||||||
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
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,6 +985,37 @@ TEST_CASE("A user filament synced to the cloud and back keeps its values and uns
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A project filament storing one value for a per-variant key applies it to every variant", "[Preset][Bundle][Regression]")
|
||||||
|
{
|
||||||
|
PresetCollection filaments(Preset::TYPE_FILAMENT, Preset::filament_options(),
|
||||||
|
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
|
||||||
|
Preset &parent = add_inmemory_preset(filaments, "Parent Filament");
|
||||||
|
parent.config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
|
||||||
|
parent.config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({"parent standard", "parent high flow"}));
|
||||||
|
parent.config.set_deserialize_strict("pressure_advance", "0.02,0.03");
|
||||||
|
parent.is_system = true;
|
||||||
|
|
||||||
|
// A filament saved before these keys became per-variant lists both variants but stores each key once.
|
||||||
|
const std::string model = "0.025,15.6,5000";
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
config.set_key_value(BBL_JSON_KEY_INHERITS, new ConfigOptionString("Parent Filament"));
|
||||||
|
config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
|
||||||
|
config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({model}));
|
||||||
|
config.set_deserialize_strict("pressure_advance", "0.016");
|
||||||
|
auto embedded = std::make_unique<Preset>(Preset::TYPE_FILAMENT, "Project Filament", false);
|
||||||
|
embedded->config = std::move(config);
|
||||||
|
embedded->is_project_embedded = true;
|
||||||
|
|
||||||
|
std::vector<Preset *> project_presets{embedded.get()};
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
|
|
||||||
|
const Preset *loaded = filaments.find_preset("Project Filament");
|
||||||
|
REQUIRE(loaded != nullptr);
|
||||||
|
CHECK(loaded->config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values == std::vector<std::string>{model, model});
|
||||||
|
check_double_vector(loaded->config.option<ConfigOptionFloats>("pressure_advance")->values, {0.016, 0.016});
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
|
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
|
||||||
{
|
{
|
||||||
ScopedTemporaryDir temp_dir;
|
ScopedTemporaryDir temp_dir;
|
||||||
|
|||||||
@@ -521,3 +521,49 @@ 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,3 +81,24 @@ 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]"));
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#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>
|
||||||
@@ -10,13 +11,17 @@
|
|||||||
#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;
|
||||||
|
|
||||||
@@ -31,6 +36,14 @@ 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]")
|
||||||
@@ -184,3 +197,59 @@ 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"));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user