Compare commits

...
Author SHA1 Message Date
Hanif Koh 99422df857 Restore Plugin HTML After a WebKit Reload
Plugin dialog content is injected with SetPage, so on the WebKit backends a
reload (context menu, keyboard shortcut or location.reload()) re-fetches the
SetPage base URL instead of the injected document, and the plugin UI is gone
for good: load_plugin_content() returned early once m_content_loaded was set.

On GTK and macOS, re-inject the plugin HTML when a main-frame load arrives
after the initial swap. m_own_page_load marks the load caused by our own
SetPage so it is not mistaken for a reload, and a post-load error is left
alone, as it only ever means a failed subresource.

MSW is left out: WebView2 reloads NavigateToString content from its own
history entry, so the page already survives a reload there, and the backend
reports in-document navigation (a page setting location.hash) as a load,
which re-injection would turn into a page wipe.
2026-09-17 15:46:14 +08:00
packerlschupfer ca668a3bc9 CLI: --inspect-paint — dump per-facet paint state as JSON (#14608)
* CLI: --inspect-paint — dump per-facet paint state as JSON

Reads the per-facet enforcer/blocker/extruder/fuzzy-skin state stored
on every ModelVolume (supported_facets / seam_facets /
mmu_segmentation_facets / fuzzy_skin_facets) and emits a structured
JSON summary to stdout. Machine-readable alternative to opening the
paint gizmos.

Per (object, volume, layer, state): facet count, surface area in
mm², and mesh-local bounding box. Empty layers collapse to
{"empty": true}. Summary at the top level rolls up totals.

One correctness detail worth calling out: FacetsAnnotation::
get_facets_strict returns an indexed_triangle_set whose `vertices`
array is the whole source mesh — only `indices` are filtered to the
painted triangles. A naive bounding_box(its) would report the whole
mesh's bbox even when only a few facets are painted. The helper
its_referenced_bbox() walks only the vertices actually indexed by
the painted triangles, so `bbox` correctly localizes the painted
region.

Rationale: every paint-driven workflow — GUI-painted .3mf verified
in CI, AI agents planning support enforcers, MMU color layout checks
— needs to know what's already painted on a model. Today that's a
GUI-only read. --inspect-paint closes that loop for scripted callers.

New file src/slic3r/Utils/PaintCLI.{hpp,cpp} (~215 lines). Depends
only on Model, TriangleMesh, TriangleSelector, FacetsAnnotation, and
nlohmann::json — all already in tree. No new dependencies, no
signature changes, no behavior change when the flag is absent.

Registered as an action (parallel to --info) so it satisfies the
"needs an action" check and bypasses the GUI fallback; control falls
through the normal post-action path to a clean exit 0.

Verification:
  unpainted STL:     every layer {"empty": true}, summary zero
  GUI-painted .3mf:  enforcer count / area / bbox match painter
  clean JSON:        parseable via jq

* CLI --inspect-paint: exit after printing, reject conflicting actions

- Finish like the end of CLI::run once the JSON is written, as the
  tooltip says. The callback manager is Linux-only, so its use is
  guarded.
- Reject actions that would otherwise be skipped without notice
  (--slice, --export-3mf, ...) before loading. Load-time options such as
  --uptodate are still accepted.
- Replace invalid UTF-8 in object names and paths instead of throwing.
- Report every input file as sources; inputs are merged into one model
  before actions run.

* CLI --inspect-paint: reject a run without input

Without an input file or --load-assemble-list there is nothing to
inspect, and the run printed nothing and exited 0. Reject it up front
with CLI_INVALID_PARAMS, next to the other invalid-parameter checks.
2026-09-17 12:01:49 +08:00
Kris Austin 6b0e190e64 ci: key the Windows compiler cache on the MSVC toolset version (#15729) 2026-09-16 18:30:18 -03:00
Kris Austin 8effa27f4a build: build the dependencies with clang-cl under the Visual Studio generator (#15673)
The deps superbuild passes the Visual Studio generator and platform to
every sub-build but not the toolset, so build_win.bat -d -l without -x
compiled every dependency with cl even though the superbuild had been
configured with -T ClangCL; CMake replaces the forwarded
CMAKE_<LANG>_COMPILER with whatever the toolset ran. The recipes that
adapt to clang-cl then disagreed with what had been built, and
wxInspector told FindwxWidgets to look in lib/clang_x64_lib while the
cl-built wxWidgets had installed into lib/vc_x64_lib:

  Could NOT find wxWidgets (missing: wxWidgets_LIBRARIES
  wxWidgets_INCLUDE_DIRS core base aui propgrid)

Forward CMAKE_GENERATOR_TOOLSET as well, so the dependencies compile
with clang-cl under MSBuild the way they already do under Ninja. Four
of them need more than that:

- OpenSSL always builds with cl, and MSBuild runs its nmake steps in
  the project's toolset environment, where ClangCL puts clang's include
  directory first and cl trips over clang's stdint.h. The project gets
  the default toolset.
- Boost.Container's dlmalloc needs -Wno-incompatible-pointer-types
  under clang. boost_container links as C++, and the Visual Studio
  generator writes only the link language's flags into the project, so
  its C file never saw CMAKE_C_FLAGS. Under that generator the option
  goes through the C++ flags as well, with the defaults kept.
- Draco's tools and NLopt's testopt compile sources their own static
  library also contains. MSBuild lists libraries before objects and
  lld-link resolves archive members as each input arrives, so the
  library's copy is pulled in before the executable's own object and
  the link fails on duplicate symbols; link.exe defers the search and
  Ninja lists the objects first. Nothing uses those executables, so
  they get /FORCE:MULTIPLE there.

The Ninja path is unchanged: the generated configure commands of all
29 dependencies are identical before and after. OCCT's arm64 override
to cl still applies under Ninja but not under the Visual Studio
generator, where the toolset wins; that combination never built and is
left for a follow-up.
2026-09-16 14:28:50 -03:00
Ian BassiandRodrigo Faselli 72774e5398 Toolchange Cyclic Order (#14868)
* Toolchange Cyclic Order

* Apply cyclic order to first layer

* Unit test

* Copilot fixes

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-16 12:19:03 -03:00
22 changed files with 503 additions and 11 deletions
+9
View File
@@ -85,6 +85,15 @@ jobs:
shell: bash shell: bash
run: | run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}" leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV" echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV" echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
+8
View File
@@ -27,8 +27,15 @@ endif ()
# Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API # Boost.Container's bundled dlmalloc passes int* where the Win32 Interlocked API
# takes volatile long*; cl compiles that with a warning, clang errors out. # takes volatile long*; cl compiles that with a warning, clang errors out.
set(_boost_c_flags_line "") set(_boost_c_flags_line "")
set(_boost_cxx_flags_line "")
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types") set(_boost_c_flags_line "-DCMAKE_C_FLAGS:STRING=-Wno-incompatible-pointer-types")
# The Visual Studio generator applies only the link language's flags to a
# project, and boost_container links as C++, so its C file never sees
# CMAKE_C_FLAGS. The C++ flags reach every file; keep CMake's defaults.
if (CMAKE_GENERATOR MATCHES "Visual Studio")
set(_boost_cxx_flags_line "-DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} -Wno-incompatible-pointer-types")
endif ()
endif () endif ()
orcaslicer_add_cmake_project(Boost orcaslicer_add_cmake_project(Boost
@@ -46,6 +53,7 @@ orcaslicer_add_cmake_project(Boost
"${_context_arch_line}" "${_context_arch_line}"
"${_context_impl_line}" "${_context_impl_line}"
"${_boost_c_flags_line}" "${_boost_c_flags_line}"
"${_boost_cxx_flags_line}"
) )
set(DEP_Boost_DEPENDS ZLIB) set(DEP_Boost_DEPENDS ZLIB)
+5
View File
@@ -184,6 +184,11 @@ function(orcaslicer_add_cmake_project projectname)
if (_dep_msvc_gen) if (_dep_msvc_gen)
set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}") set(_gen CMAKE_GENERATOR "${DEP_MSVC_GEN}" CMAKE_GENERATOR_PLATFORM "${DEP_PLATFORM}")
# The toolset picks the compiler here, not the CMAKE_<LANG>_COMPILER
# forwarded below, so without it a clang-cl superbuild builds with cl.
if (CMAKE_GENERATOR_TOOLSET)
list(APPEND _gen CMAKE_GENERATOR_TOOLSET "${CMAKE_GENERATOR_TOOLSET}")
endif ()
else() else()
set(_gen "") set(_gen "")
endif() endif()
+3
View File
@@ -7,4 +7,7 @@ orcaslicer_add_cmake_project(Draco
${_options} ${_options}
URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip URL https://github.com/google/draco/archive/refs/tags/1.5.7.zip
URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77 URL_HASH SHA256=27b72ba2d5ff3d0a9814ad40d4cb88f8dc89a35491c0866d952473f8f9416b77
CMAKE_ARGS
# The encoder and decoder tools duplicate draco.lib; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
+2
View File
@@ -8,6 +8,8 @@ orcaslicer_add_cmake_project(NLopt
-DNLOPT_GUILE:BOOL=OFF -DNLOPT_GUILE:BOOL=OFF
-DNLOPT_SWIG:BOOL=OFF -DNLOPT_SWIG:BOOL=OFF
-DNLOPT_TESTS:BOOL=OFF -DNLOPT_TESTS:BOOL=OFF
# testopt is built regardless of NLOPT_TESTS; see deps-windows.cmake.
"${DEP_LLD_FORCE_MULTIPLE}"
) )
if (MSVC) if (MSVC)
+6
View File
@@ -80,6 +80,12 @@ ExternalProject_Add(dep_OpenSSL
INSTALL_COMMAND ${_install_cmd} INSTALL_COMMAND ${_install_cmd}
) )
if (CMAKE_GENERATOR MATCHES "Visual Studio")
# OpenSSL builds with cl, but MSBuild runs nmake in this project's toolset
# environment, and ClangCL's puts clang's headers first. Use the default.
set_target_properties(dep_OpenSSL PROPERTIES VS_PLATFORM_TOOLSET "$(DefaultPlatformToolset)")
endif ()
ExternalProject_Add_Step(dep_OpenSSL install_cmake_files ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
DEPENDEES install DEPENDEES install
+9
View File
@@ -42,6 +42,15 @@ else ()
message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}") message(FATAL_ERROR "Unsupported OS architecture: ${DEPS_ARCH}")
endif () endif ()
# Draco's tools and NLopt's testopt compile sources that are also in their
# static library. MSBuild passes the library before the objects and lld-link
# resolves as it goes, so the library's copy wins and the object then reads as
# a duplicate. Nothing uses those executables, so let lld keep the first one.
set(DEP_LLD_FORCE_MULTIPLE "")
if (CMAKE_GENERATOR MATCHES "Visual Studio" AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DEP_LLD_FORCE_MULTIPLE "-DCMAKE_EXE_LINKER_FLAGS:STRING=${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE")
endif ()
if (${DEP_DEBUG}) if (${DEP_DEBUG})
set(DEP_BOOST_DEBUG "debug") set(DEP_BOOST_DEBUG "debug")
else () else ()
+48
View File
@@ -87,6 +87,7 @@ using namespace nlohmann;
#include "dev-utils/BaseException.h" #include "dev-utils/BaseException.h"
#endif #endif
#include "slic3r/Utils/MeshInspect.hpp" #include "slic3r/Utils/MeshInspect.hpp"
#include "slic3r/Utils/PaintCLI.hpp"
#include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/BitmapCache.hpp" #include "slic3r/GUI/BitmapCache.hpp"
#include "slic3r/GUI/OpenGLManager.hpp" #include "slic3r/GUI/OpenGLManager.hpp"
@@ -1443,6 +1444,29 @@ int CLI::run(int argc, char **argv)
} }
} }
// --inspect-paint prints its JSON and exits, so any action that does work of its
// own (slicing, exporting) would be skipped without notice. Reject those up front;
// only options that merely tune how the input is loaded may come along.
if (std::find(m_actions.begin(), m_actions.end(), "inspect_paint") != m_actions.end()) {
static const std::set<std::string> inspect_compatible = { "inspect_paint", "uptodate", "load_defaultfila", "min_save",
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
for (const std::string &action : m_actions) {
if (inspect_compatible.count(action) == 0) {
std::string flag = action;
std::replace(flag.begin(), flag.end(), '_', '-');
boost::nowide::cerr << "--inspect-paint cannot be combined with --" << flag << std::endl;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
}
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
boost::nowide::cerr << "--inspect-paint needs an input file or --load-assemble-list" << std::endl;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
}
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there // --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is // too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
// sliced or exported. // sliced or exported.
@@ -6100,6 +6124,30 @@ int CLI::run(int argc, char **argv)
cli_status_callback(slicing_status); cli_status_callback(slicing_status);
} }
g_cli_callback_mgr.stop(); g_cli_callback_mgr.stop();
#endif
for (Model &m : m_models)
m.remove_backup_path_if_exist();
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
boost::nowide::cerr.flush();
return CLI_SUCCESS;
} else if (opt_key == "inspect_paint") {
// --inspect-paint — read the per-facet enforcer/blocker/extruder/
// fuzzy state from the loaded model and emit a JSON summary.
// Machine-readable alternative to opening the paint gizmos.
for (Model &model : m_models) {
model.add_default_instances();
Slic3r::PaintCLI::inspect_to_json(model, m_input_files, boost::nowide::cout);
}
boost::nowide::cout.flush();
// The tooltip promises "then exit"; conflicting actions were rejected before
// loading. Finish like the end of run(). flush_and_exit() is not usable here:
// it prints "found error ..." to stdout, which would corrupt the JSON.
#if defined(__linux__) || defined(__LINUX__)
if (g_cli_callback_mgr.is_started()) {
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
cli_status_callback(slicing_status);
}
g_cli_callback_mgr.stop();
#endif #endif
for (Model &m : m_models) for (Model &m : m_models)
m.remove_backup_path_if_exist(); m.remove_backup_path_if_exist();
+60 -5
View File
@@ -2735,6 +2735,28 @@ void ToolOrdering::enforce_mixed_component_order()
} }
} }
// Declared in ToolOrdering.hpp (exposed for unit testing).
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
{
std::vector<unsigned int> order;
for (const std::string& token : split_string(str, ',')) {
try {
size_t pos = 0;
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
// consumed (bar trailing whitespace) to drop it like any other garbage.
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
continue;
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
order.emplace_back((unsigned int)(filament - 1));
} catch (const std::exception&) {
// Not a number, ignore it.
}
}
return order;
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer) void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{ {
const PrintConfig* print_config = m_print_config_ptr; const PrintConfig* print_config = m_print_config_ptr;
@@ -2832,11 +2854,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
const bool use_cyclic_ordering = const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic); (print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
// sequence); the cyclic sequence is only forced onto it when the user opts in.
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
// still yields the default cyclic order.
const std::vector<unsigned int> cyclic_order =
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
: std::vector<unsigned int>();
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
// after the listed ones.
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
std::sort(filaments.begin(), filaments.end());
if (!cyclic_order.empty())
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
auto rank = [&cyclic_order](unsigned int filament) {
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
};
return rank(lhs) < rank(rhs);
});
};
// other_layers_seq: the layer_idx and extruder_idx are base on 1 // other_layers_seq: the layer_idx and extruder_idx are base on 1
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool { auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
if (!reorder_first_layer && layer_idx == 0) { if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size()); // The first layer tool order is already decided (adhesion-optimized, plus any custom first
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; }); // layer sequence). Only override it with the cyclic sequence when the user opted in.
std::vector<unsigned int> ordered = first_layer_filaments;
if (cyclic_first_layer)
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
return true; return true;
} }
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) { for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
@@ -2847,9 +2899,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
} }
} }
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) { // Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
&& size_t(layer_idx) < layer_filaments.size()) {
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)]; std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
std::sort(ordered.begin(), ordered.end()); apply_cyclic_order(ordered);
out_seq.resize(ordered.size()); out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; }); std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true; return true;
+5
View File
@@ -417,6 +417,11 @@ private:
int most_used_extruder; int most_used_extruder;
}; };
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
// still orders the filaments it does name. Exposed for unit testing.
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
} // namespace SLic3r } // namespace SLic3r
#endif /* slic3r_ToolOrdering_hpp_ */ #endif /* slic3r_ToolOrdering_hpp_ */
+2
View File
@@ -1320,6 +1320,8 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_extra_flow", "wipe_tower_extra_flow",
"single_extruder_multi_material_priming", "single_extruder_multi_material_priming",
"toolchange_ordering", "toolchange_ordering",
"toolchange_cyclic_order",
"toolchange_cyclic_first_layer",
"wipe_tower_rotation_angle", "wipe_tower_rotation_angle",
"tree_support_branch_distance_organic", "tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic", "tree_support_branch_diameter_organic",
+2
View File
@@ -360,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "other_layers_print_sequence" || opt_key == "other_layers_print_sequence"
|| opt_key == "other_layers_print_sequence_nums" || opt_key == "other_layers_print_sequence_nums"
|| opt_key == "toolchange_ordering" || opt_key == "toolchange_ordering"
|| opt_key == "toolchange_cyclic_order"
|| opt_key == "toolchange_cyclic_first_layer"
|| opt_key == "extruder_ams_count" || opt_key == "extruder_ams_count"
|| opt_key == "extruder_nozzle_stats" || opt_key == "extruder_nozzle_stats"
|| opt_key == "filament_map_mode" || opt_key == "filament_map_mode"
+41
View File
@@ -6700,6 +6700,34 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cyclic")); def->enum_labels.emplace_back(L("Cyclic"));
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default)); def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
def = this->add("toolchange_cyclic_order", coString);
def->label = L("Cyclic order");
def->category = L("Advanced");
def->tooltip = L(
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
"Leave empty to cycle through the filaments in ascending order."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionString(""));
def = this->add("toolchange_cyclic_first_layer", coBool);
def->label = L("Apply cyclic order to first layer");
def->category = L("Advanced");
def->tooltip = L(
"Applies the cyclic toolchange order to the first layer as well.\n"
"By default this is disabled, because the first layer is instead ordered for the best bed "
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
"to the first layer, which is printed slowly and hot for adhesion.\n"
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
"the cost of that adhesion optimization."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("slice_closing_radius", coFloat); def = this->add("slice_closing_radius", coFloat);
def->label = L("Slice gap closing radius"); def->label = L("Slice gap closing radius");
def->category = L("Quality"); def->category = L("Quality");
@@ -11963,6 +11991,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
"the --ground-* options choose from. Machine-readable alternative to --info."); "the --ground-* options choose from. Machine-readable alternative to --info.");
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
// paint state stored on the loaded model (supports, seam, MMU color,
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
// reason about existing paint on a .3mf without loading the GUI.
def = this->add("inspect_paint", coBool);
def->label = L("Inspect paint (JSON to stdout)");
def->tooltip = L("Print a structured JSON summary of every painted layer "
"(supports, seam, MMU color, fuzzy-skin) already stored on "
"the loaded model \u2014 per-state facet count, surface area, "
"and mesh-local bounding box \u2014 then exit. Machine-readable "
"alternative to opening the paint gizmos in the GUI.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("export_settings", coString); def = this->add("export_settings", coString);
def->label = L("Export Settings"); def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file. Use - to write them to stdout."); def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
+2
View File
@@ -1627,6 +1627,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, manual_filament_change)) ((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming)) ((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering)) ((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode)) ((ConfigOptionString, change_extrusion_role_gcode))
+2
View File
@@ -682,6 +682,8 @@ set(SLIC3R_GUI_SOURCES
Utils/Bonjour.hpp Utils/Bonjour.hpp
Utils/MeshInspect.cpp Utils/MeshInspect.cpp
Utils/MeshInspect.hpp Utils/MeshInspect.hpp
Utils/PaintCLI.cpp
Utils/PaintCLI.hpp
Utils/CalibUtils.cpp Utils/CalibUtils.cpp
Utils/CalibUtils.hpp Utils/CalibUtils.hpp
Utils/ColorSpaceConvert.cpp Utils/ColorSpaceConvert.cpp
+4
View File
@@ -1055,6 +1055,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2); toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM)); toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"}) for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
+19 -6
View File
@@ -139,19 +139,32 @@ void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event) void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
{ {
// The first bootstrap load (or its error) triggers the swap to plugin HTML; // The first bootstrap load (or its error) triggers the swap to plugin HTML.
// the resulting plugin-page load is ignored (guarded by m_content_loaded). if (!m_content_loaded)
load_plugin_content(); load_plugin_content();
#ifndef __WXMSW__
// WebKit reloads the SetPage base URL rather than the injected page, so a later main-frame
// load that is not our own SetPage is a browser reload, and the plugin HTML has to be put
// back. A post-load error only ever means a failed subresource. WebView2 reloads SetPage
// content from its own history entry, and reports in-document navigation as a load, so
// MSW needs neither the re-injection nor a guard against it.
else if (event.GetEventType() == wxEVT_WEBVIEW_LOADED) {
if (m_own_page_load)
m_own_page_load = false;
else
load_plugin_content();
}
#endif
event.Skip(); event.Skip();
} }
void PluginWebDialog::load_plugin_content() void PluginWebDialog::load_plugin_content()
{ {
if (m_content_loaded)
return;
m_content_loaded = true; m_content_loaded = true;
if (wxWebView* wv = browser()) if (wxWebView* wv = browser()) {
m_own_page_load = true;
wv->SetPage(wxString::FromUTF8(m_html), web_base_url()); wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
}
} }
void PluginWebDialog::on_script_message(const nlohmann::json& payload) void PluginWebDialog::on_script_message(const nlohmann::json& payload)
+1
View File
@@ -72,6 +72,7 @@ private:
std::string m_html; std::string m_html;
bool m_content_loaded{false}; bool m_content_loaded{false};
bool m_own_page_load{false}; // a SetPage of the plugin HTML is in flight
bool m_open{true}; bool m_open{true};
bool m_close_fired{false}; bool m_close_fired{false};
std::optional<nlohmann::json> m_result; std::optional<nlohmann::json> m_result;
+2
View File
@@ -3026,6 +3026,8 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Advanced"), L"advanced"); optgroup = page->new_optgroup(L("Advanced"), L"advanced");
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam"); optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering"); optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering");
optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells"); optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells");
optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region"); optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region");
optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region"); optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
+204
View File
@@ -0,0 +1,204 @@
// PaintCLI.cpp — CLI paint-inspection primitives. See PaintCLI.hpp.
#include "PaintCLI.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include <nlohmann/json.hpp>
#include <cmath>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
namespace PaintCLI {
namespace {
using json = nlohmann::json;
double its_surface_area(const indexed_triangle_set &its)
{
double total = 0.0;
for (const stl_triangle_vertex_indices &t : its.indices) {
const Vec3f &a = its.vertices[t(0)];
const Vec3f &b = its.vertices[t(1)];
const Vec3f &c = its.vertices[t(2)];
total += 0.5 * (b - a).cross(c - a).norm();
}
return total;
}
// Bbox over triangle-referenced vertices only. get_facets_strict() returns
// an itset with the full source vertex list — using bounding_box() on it
// would report the whole mesh's bbox even when only a few facets are painted.
BoundingBoxf3 its_referenced_bbox(const indexed_triangle_set &its)
{
BoundingBoxf3 bb;
bool first = true;
for (const stl_triangle_vertex_indices &t : its.indices) {
for (int k = 0; k < 3; ++k) {
const Vec3d v = its.vertices[t(k)].cast<double>();
if (first) { bb.min = bb.max = v; first = false; }
else bb.merge(v);
}
}
return bb;
}
json vec3_to_json(const Vec3d &v)
{
return json::array({ v.x(), v.y(), v.z() });
}
json bbox_to_json(const BoundingBoxf3 &bb)
{
return {
{ "min", vec3_to_json(bb.min) },
{ "max", vec3_to_json(bb.max) },
{ "size", vec3_to_json(Vec3d(bb.max - bb.min)) },
};
}
// One (layer, state) row — empty ones are omitted at the caller level.
json state_entry(const std::string &label, const indexed_triangle_set &its)
{
return {
{ "state", label },
{ "facets", its.indices.size() },
{ "area_mm2", its_surface_area(its) },
{ "bbox", bbox_to_json(its_referenced_bbox(its)) },
};
}
// Iterate the states relevant to one FacetsAnnotation kind, collecting
// non-empty entries. Empty layer → {"empty": true}. `n_facets_out` is the
// running total of painted facets — bumped for the summary.
json inspect_layer(const ModelVolume &mv, const FacetsAnnotation &fa,
const std::vector<std::pair<EnforcerBlockerType, std::string>> &states,
size_t &n_facets_out)
{
if (fa.empty())
return { { "empty", true } };
json entries = json::array();
for (const auto &st : states) {
if (!fa.has_facets(mv, st.first))
continue;
indexed_triangle_set its = fa.get_facets_strict(mv, st.first);
if (its.indices.empty())
continue;
n_facets_out += its.indices.size();
entries.push_back(state_entry(st.second, its));
}
return {
{ "empty", entries.empty() },
{ "states", std::move(entries) },
};
}
const std::vector<std::pair<EnforcerBlockerType, std::string>> &supports_states()
{
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
{ EnforcerBlockerType::ENFORCER, "ENFORCER" },
{ EnforcerBlockerType::BLOCKER, "BLOCKER" },
};
return s;
}
const std::vector<std::pair<EnforcerBlockerType, std::string>> &fuzzy_states()
{
// FUZZY_SKIN is an enum alias for ENFORCER; the layer is single-state.
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
{ EnforcerBlockerType::FUZZY_SKIN, "FUZZY_SKIN" },
};
return s;
}
const std::vector<std::pair<EnforcerBlockerType, std::string>> &mmu_states()
{
static std::vector<std::pair<EnforcerBlockerType, std::string>> s = []{
std::vector<std::pair<EnforcerBlockerType, std::string>> v;
for (int i = 1; i <= int(EnforcerBlockerType::ExtruderMax); ++i)
v.emplace_back(EnforcerBlockerType(i), "extruder_" + std::to_string(i));
return v;
}();
return s;
}
} // namespace
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
std::ostream &out)
{
json root;
root["sources"] = source_paths;
root["frame"] = "mesh_local";
root["note"] = "Coordinates are mesh-local (each volume's own frame). "
"Paint gizmos operate in this frame.";
json objects = json::array();
size_t total_objects = 0, total_volumes = 0, total_painted = 0, total_facets = 0;
for (size_t oi = 0; oi < model.objects.size(); ++oi) {
const ModelObject *mo = model.objects[oi];
if (!mo) continue;
++total_objects;
json obj;
obj["index"] = oi;
obj["name"] = mo->name;
json volumes = json::array();
for (size_t vi = 0; vi < mo->volumes.size(); ++vi) {
const ModelVolume *mv = mo->volumes[vi];
if (!mv) continue;
++total_volumes;
const indexed_triangle_set &its = mv->mesh().its;
json vol;
vol["index"] = vi;
vol["name"] = mv->name;
vol["n_facets"] = its.indices.size();
vol["is_model_part"] = mv->is_model_part();
vol["bbox_mesh_local"] = bbox_to_json(bounding_box(its));
size_t vol_painted = 0;
json paints;
paints["supports"] = inspect_layer(*mv, mv->supported_facets,
supports_states(), vol_painted);
paints["seam"] = inspect_layer(*mv, mv->seam_facets,
supports_states(), vol_painted);
paints["mmu_segmentation"] = inspect_layer(*mv, mv->mmu_segmentation_facets,
mmu_states(), vol_painted);
paints["fuzzy_skin"] = inspect_layer(*mv, mv->fuzzy_skin_facets,
fuzzy_states(), vol_painted);
vol["paints"] = std::move(paints);
vol["painted_facets_total"] = vol_painted;
if (vol_painted > 0) ++total_painted;
total_facets += vol_painted;
volumes.push_back(std::move(vol));
}
obj["volumes"] = std::move(volumes);
objects.push_back(std::move(obj));
}
root["objects"] = std::move(objects);
root["summary"] = {
{ "objects", total_objects },
{ "volumes", total_volumes },
{ "volumes_with_paint", total_painted },
{ "painted_facets_total", total_facets },
};
// Object names and file paths are arbitrary bytes, and dump() throws on invalid
// UTF-8 by default. Replace such sequences with U+FFFD so the output is always
// valid JSON rather than an exception out of the CLI.
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
}
} // namespace PaintCLI
} // namespace Slic3r
+31
View File
@@ -0,0 +1,31 @@
// PaintCLI.hpp — CLI paint-inspection primitives.
//
// Backs the --inspect-paint CLI action. Reads the per-facet enforcer /
// blocker / extruder / fuzzy-skin state that OrcaSlicer stores on every
// ModelVolume (supports, seam, MMU color, fuzzy-skin) and emits a
// structured JSON summary — facet count, surface area, and mesh-local
// bounding box per state — so CI / scripted / AI tooling can reason
// about existing paint on a .3mf without opening the GUI.
//
// Coordinates are mesh-local (each volume's own frame), matching the
// frame that the paint gizmos operate in.
#ifndef slic3r_PaintCLI_hpp_
#define slic3r_PaintCLI_hpp_
#include <iosfwd>
#include <string>
#include <vector>
namespace Slic3r {
class Model;
namespace PaintCLI {
// `source_paths` lists every input file; the CLI merges them into one Model.
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
std::ostream &out);
} // namespace PaintCLI
} // namespace Slic3r
#endif
@@ -1025,3 +1025,41 @@ TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2
REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED);
REQUIRE(print.is_step_done(psSlicingFinished)); REQUIRE(print.is_step_done(psSlicingFinished));
} }
TEST_CASE("parse_cyclic_order parses user cyclic toolchange sequences", "[ToolOrdering][Cyclic]")
{
// Filament numbers are 1-based in the UI; the parser returns 0-based indices.
SECTION("well-formed sequence") {
REQUIRE(parse_cyclic_order("3,2,1,4", 4) == std::vector<unsigned int>({2, 1, 0, 3}));
}
SECTION("surrounding whitespace is tolerated") {
REQUIRE(parse_cyclic_order(" 3 , 2 ,1, 4 ", 4) == std::vector<unsigned int>({2, 1, 0, 3}));
}
SECTION("out-of-range and non-positive entries are dropped") {
// 0 is below the 1-based range, 5 is above it for a 4-filament setup, -1 is invalid.
REQUIRE(parse_cyclic_order("0,5,-1,2", 4) == std::vector<unsigned int>({1}));
}
SECTION("duplicates keep only the first occurrence") {
REQUIRE(parse_cyclic_order("2,2,1,2", 4) == std::vector<unsigned int>({1, 0}));
}
SECTION("garbage tokens are ignored") {
REQUIRE(parse_cyclic_order("3,abc,,2,x1", 4) == std::vector<unsigned int>({2, 1}));
}
SECTION("tokens that only start with a number are ignored") {
// "2x" must be dropped rather than parsed as filament 2.
REQUIRE(parse_cyclic_order("3,2x,1", 4) == std::vector<unsigned int>({2, 0}));
}
SECTION("empty string yields an empty order") {
REQUIRE(parse_cyclic_order("", 4).empty());
}
SECTION("a partial sequence only names the filaments it lists") {
REQUIRE(parse_cyclic_order("3,1", 4) == std::vector<unsigned int>({2, 0}));
}
}