mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-17 22:12:36 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2403855d93 | ||
|
|
ca668a3bc9 | ||
|
|
6b0e190e64 | ||
|
|
8effa27f4a | ||
|
|
72774e5398 |
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Vendored
+8
@@ -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)
|
||||||
|
|||||||
Vendored
+5
@@ -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()
|
||||||
|
|||||||
Vendored
+3
@@ -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}"
|
||||||
)
|
)
|
||||||
Vendored
+2
@@ -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)
|
||||||
|
|||||||
Vendored
+6
@@ -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
|
||||||
|
|
||||||
|
|||||||
Vendored
+9
@@ -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 ()
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.12"
|
||||||
|
#
|
||||||
|
# [tool.orcaslicer.plugin]
|
||||||
|
# name = "Dock Panel Demo"
|
||||||
|
# description = "Opens a dockable panel beside the 3D view that lists the objects on the plate."
|
||||||
|
# author = "OrcaSlicer"
|
||||||
|
# version = "0.0.1"
|
||||||
|
# ///
|
||||||
|
"""Dock Panel Demo -- orca.host.ui.create_dock_panel().
|
||||||
|
|
||||||
|
Run it from the Plugins dialog. It opens an HTML panel docked on the right of the 3D view, in the
|
||||||
|
same dock area as the sidebar. Drag its caption to dock it on another side (or float it, where the
|
||||||
|
platform allows), hide it from the page and run the plugin again to bring it back, or close it with
|
||||||
|
its close button or from the page.
|
||||||
|
|
||||||
|
page --orca.postMessage({command: 'refresh'})--> plugin.on_message()
|
||||||
|
page --orca.postMessage({command: 'hide'})--> plugin.on_message() -> panel.hide()
|
||||||
|
page --orca.close()--> panel closes, plugin.on_close()
|
||||||
|
plugin --panel.post({command: 'objects', ...})--> page (orca.onMessage)
|
||||||
|
"""
|
||||||
|
import orca
|
||||||
|
|
||||||
|
PAGE = """
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 12px; font-size: 13px; }
|
||||||
|
h3 { margin: 0 0 4px; font-size: 14px; }
|
||||||
|
.note { margin: 0 0 12px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.actions button.quiet { background: transparent; color: var(--orca-fg); border-color: var(--orca-border); }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
td.count { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
#status { margin-top: 10px; color: var(--orca-muted); font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h3>Objects on the plate</h3>
|
||||||
|
<p class="note">Docked beside the 3D view. Drag the caption to move it.</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" id="refresh">Refresh</button>
|
||||||
|
<button type="button" id="hide" class="quiet">Hide</button>
|
||||||
|
<button type="button" id="close" class="quiet">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Name</th><th>Parts</th><th>Copies</th></tr></thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
<p id="status">Waiting for the plugin...</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function text(value) {
|
||||||
|
var span = document.createElement("span");
|
||||||
|
span.textContent = value;
|
||||||
|
return span.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(message) {
|
||||||
|
var rows = document.getElementById("rows");
|
||||||
|
var status = document.getElementById("status");
|
||||||
|
if (message.error) {
|
||||||
|
rows.innerHTML = "";
|
||||||
|
status.textContent = message.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.innerHTML = message.objects.map(function (object) {
|
||||||
|
return "<tr><td>" + text(object.name) + "</td><td class=\\"count\\">" + object.volumes +
|
||||||
|
"</td><td class=\\"count\\">" + object.instances + "</td></tr>";
|
||||||
|
}).join("");
|
||||||
|
status.textContent = message.objects.length + " object(s), refreshed " + new Date().toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
orca.onMessage(function (message) {
|
||||||
|
if (message && message.command === "objects")
|
||||||
|
render(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("refresh").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
});
|
||||||
|
document.getElementById("hide").addEventListener("click", function () {
|
||||||
|
orca.postMessage({ command: "hide" });
|
||||||
|
});
|
||||||
|
document.getElementById("close").addEventListener("click", function () {
|
||||||
|
orca.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
orca.postMessage({ command: "refresh" });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def plate_objects():
|
||||||
|
try:
|
||||||
|
model = orca.host.model()
|
||||||
|
except RuntimeError as error:
|
||||||
|
return {"command": "objects", "error": str(error)}
|
||||||
|
return {
|
||||||
|
"command": "objects",
|
||||||
|
"objects": [
|
||||||
|
{"name": obj.name or "(unnamed)", "volumes": obj.volume_count(), "instances": obj.instance_count()}
|
||||||
|
for obj in model.objects()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DockPanelDemo(orca.script.ScriptPluginCapabilityBase):
|
||||||
|
panel = None
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return "Dock Panel Demo"
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
# The capability instance lives as long as the plugin, so a second run finds the open panel.
|
||||||
|
if self.panel is not None and self.panel.is_open():
|
||||||
|
self.panel.show()
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo is already open.")
|
||||||
|
self.panel = orca.host.ui.create_dock_panel(
|
||||||
|
html=PAGE,
|
||||||
|
title="Dock Panel Demo",
|
||||||
|
width=320,
|
||||||
|
height=480,
|
||||||
|
on_message=self.on_message,
|
||||||
|
on_close=self.on_close,
|
||||||
|
dock="right",
|
||||||
|
)
|
||||||
|
return orca.ExecutionResult.success("Dock Panel Demo opened.")
|
||||||
|
|
||||||
|
# Called on the UI thread when the page posts.
|
||||||
|
def on_message(self, message):
|
||||||
|
command = (message or {}).get("command")
|
||||||
|
if command == "refresh":
|
||||||
|
self.panel.post(plate_objects())
|
||||||
|
elif command == "hide":
|
||||||
|
self.panel.hide()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
self.panel = None
|
||||||
|
|
||||||
|
|
||||||
|
@orca.plugin
|
||||||
|
class DockPanelDemoPlugin(orca.base):
|
||||||
|
def register_capabilities(self):
|
||||||
|
orca.register_capability(DockPanelDemo)
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_ */
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ set(SLIC3R_GUI_SOURCES
|
|||||||
GUI/PluginProgressDialog.hpp
|
GUI/PluginProgressDialog.hpp
|
||||||
GUI/PluginWebDialog.cpp
|
GUI/PluginWebDialog.cpp
|
||||||
GUI/PluginWebDialog.hpp
|
GUI/PluginWebDialog.hpp
|
||||||
|
GUI/PluginDockPanel.cpp
|
||||||
|
GUI/PluginDockPanel.hpp
|
||||||
|
GUI/PluginWebPanel.cpp
|
||||||
|
GUI/PluginWebPanel.hpp
|
||||||
GUI/DragCanvas.cpp
|
GUI/DragCanvas.cpp
|
||||||
GUI/DragCanvas.hpp
|
GUI/DragCanvas.hpp
|
||||||
GUI/EditGCodeDialog.cpp
|
GUI/EditGCodeDialog.cpp
|
||||||
@@ -682,6 +686,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
|
||||||
|
|||||||
@@ -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"})
|
||||||
|
|||||||
@@ -1114,6 +1114,8 @@ void MainFrame::shutdown()
|
|||||||
if (m_project != nullptr)
|
if (m_project != nullptr)
|
||||||
m_project->shutdown();
|
m_project->shutdown();
|
||||||
m_plugin_pages.shutdown();
|
m_plugin_pages.shutdown();
|
||||||
|
if (m_plater != nullptr)
|
||||||
|
m_plater->remove_plugin_panes();
|
||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
// Edge panels are child windows — wxWidgets destroys them automatically.
|
// Edge panels are child windows — wxWidgets destroys them automatically.
|
||||||
m_edge_bottom = nullptr;
|
m_edge_bottom = nullptr;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
#ifdef __WXGTK__
|
#ifdef __WXGTK__
|
||||||
#include "LinuxDisplayBackend.hpp"
|
#include "LinuxDisplayBackend.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
#include "PluginDockPanel.hpp"
|
||||||
#include "GUI_Utils.hpp"
|
#include "GUI_Utils.hpp"
|
||||||
#include "GUI_Factories.hpp"
|
#include "GUI_Factories.hpp"
|
||||||
#include "wxExtensions.hpp"
|
#include "wxExtensions.hpp"
|
||||||
@@ -6720,6 +6721,14 @@ struct Plater::priv
|
|||||||
|
|
||||||
// GUI elements
|
// GUI elements
|
||||||
AuiMgr m_aui_mgr;
|
AuiMgr m_aui_mgr;
|
||||||
|
// Live plugin panes. `on_close` runs when the user closes one from its close button; `shown` is
|
||||||
|
// what the plugin asked for.
|
||||||
|
struct PluginPane
|
||||||
|
{
|
||||||
|
std::function<void()> on_close;
|
||||||
|
bool shown{true};
|
||||||
|
};
|
||||||
|
std::map<wxWindow*, PluginPane> m_plugin_panes;
|
||||||
wxString m_default_window_layout;
|
wxString m_default_window_layout;
|
||||||
wxPanel* current_panel{ nullptr };
|
wxPanel* current_panel{ nullptr };
|
||||||
std::vector<wxPanel*> panels;
|
std::vector<wxPanel*> panels;
|
||||||
@@ -6891,6 +6900,11 @@ struct Plater::priv
|
|||||||
void update_sidebar(bool force_update = false);
|
void update_sidebar(bool force_update = false);
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
Sidebar::DockingState get_sidebar_docking_state();
|
Sidebar::DockingState get_sidebar_docking_state();
|
||||||
|
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_plugin_pane(wxWindow* window);
|
||||||
|
void show_plugin_pane(wxWindow* window, bool show);
|
||||||
|
bool plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const;
|
||||||
|
|
||||||
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); }
|
||||||
|
|
||||||
@@ -7471,6 +7485,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
|
|||||||
panel_3d->SetSizer(panel_sizer);
|
panel_3d->SetSizer(panel_sizer);
|
||||||
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false));
|
||||||
|
|
||||||
|
q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) {
|
||||||
|
const wxAuiPaneInfo* pane = evt.GetPane();
|
||||||
|
auto it = pane != nullptr ? m_plugin_panes.find(pane->window) : m_plugin_panes.end();
|
||||||
|
if (it != m_plugin_panes.end()) {
|
||||||
|
const std::function<void()> on_close = std::move(it->second.on_close);
|
||||||
|
m_plugin_panes.erase(it);
|
||||||
|
if (on_close)
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
m_default_window_layout = m_aui_mgr.SavePerspective();
|
m_default_window_layout = m_aui_mgr.SavePerspective();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -8141,6 +8167,14 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto& [window, plugin_pane] : m_plugin_panes) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() != plugin_pane_visible(plugin_pane, pane)) {
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
needs_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (needs_update) {
|
if (needs_update) {
|
||||||
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
notification_manager->set_sidebar_collapsed(sidebar.IsShown());
|
||||||
m_aui_mgr.Update();
|
m_aui_mgr.Update();
|
||||||
@@ -8150,10 +8184,95 @@ void Plater::priv::update_sidebar(bool force_update) {
|
|||||||
void Plater::priv::reset_window_layout()
|
void Plater::priv::reset_window_layout()
|
||||||
{
|
{
|
||||||
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
m_aui_mgr.LoadPerspective(m_default_window_layout, false);
|
||||||
|
// Loading a layout hides every pane it does not list, and the default layout lists no plugin panes.
|
||||||
|
for (const auto& [window, plugin_pane] : m_plugin_panes)
|
||||||
|
if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk())
|
||||||
|
pane.Show(plugin_pane.shown);
|
||||||
sidebar_layout.is_collapsed = false;
|
sidebar_layout.is_collapsed = false;
|
||||||
update_sidebar(true);
|
update_sidebar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Plater::priv::plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const
|
||||||
|
{
|
||||||
|
// A floating pane is a top-level window, so it does not hide with the Plater on other tabs.
|
||||||
|
return plugin_pane.shown && (!pane.IsFloating() || sidebar_layout.show);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
const wxString base_name = wxString::FromUTF8(name);
|
||||||
|
wxString unique_name = base_name;
|
||||||
|
for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i)
|
||||||
|
unique_name = base_name + wxString::Format("#%d", i);
|
||||||
|
|
||||||
|
// A restored layout below already holds pixels.
|
||||||
|
const wxSize pixels = q->FromDIP(size);
|
||||||
|
wxAuiPaneInfo info;
|
||||||
|
info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true);
|
||||||
|
if (dock == "left")
|
||||||
|
info.Left();
|
||||||
|
else if (dock == "bottom")
|
||||||
|
info.Bottom();
|
||||||
|
else
|
||||||
|
info.Right();
|
||||||
|
if (dock == "float")
|
||||||
|
info.Float();
|
||||||
|
|
||||||
|
// Put the pane back where it was the last time the window layout was saved with it open.
|
||||||
|
const std::string saved = plugin_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string());
|
||||||
|
if (!saved.empty()) {
|
||||||
|
m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info);
|
||||||
|
info.Caption(caption).DestroyOnClose(true).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating is disabled on Wayland.
|
||||||
|
if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) {
|
||||||
|
info.Dock().Floatable(false);
|
||||||
|
if (info.dock_direction == wxAUI_DOCK_NONE)
|
||||||
|
info.Right();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginPane& plugin_pane = m_plugin_panes[window] = PluginPane{std::move(on_close)};
|
||||||
|
info.Show(plugin_pane_visible(plugin_pane, info));
|
||||||
|
m_aui_mgr.AddPane(window, info);
|
||||||
|
|
||||||
|
// wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar
|
||||||
|
// does, for the saved layout.
|
||||||
|
window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) {
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) {
|
||||||
|
const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM;
|
||||||
|
pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(),
|
||||||
|
horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight());
|
||||||
|
}
|
||||||
|
evt.Skip();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::remove_plugin_pane(wxWindow* window)
|
||||||
|
{
|
||||||
|
m_plugin_panes.erase(window);
|
||||||
|
if (m_aui_mgr.DetachPane(window))
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
window->Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Plater::priv::show_plugin_pane(wxWindow* window, bool show)
|
||||||
|
{
|
||||||
|
const auto it = m_plugin_panes.find(window);
|
||||||
|
wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window);
|
||||||
|
if (it == m_plugin_panes.end() || !pane.IsOk())
|
||||||
|
return;
|
||||||
|
it->second.shown = show;
|
||||||
|
if (pane.IsShown() == plugin_pane_visible(it->second, pane))
|
||||||
|
return;
|
||||||
|
pane.Show(!pane.IsShown());
|
||||||
|
m_aui_mgr.Update();
|
||||||
|
}
|
||||||
|
|
||||||
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
Sidebar::DockingState Plater::priv::get_sidebar_docking_state() {
|
||||||
if (!sidebar_layout.is_enabled) {
|
if (!sidebar_layout.is_enabled) {
|
||||||
return Sidebar::None;
|
return Sidebar::None;
|
||||||
@@ -17712,6 +17831,19 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
|
|||||||
|
|
||||||
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
void Plater::reset_window_layout() { p->reset_window_layout(); }
|
||||||
|
|
||||||
|
void Plater::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close)
|
||||||
|
{
|
||||||
|
p->add_plugin_pane(window, name, caption, dock, size, std::move(on_close));
|
||||||
|
}
|
||||||
|
void Plater::remove_plugin_pane(wxWindow* window) { p->remove_plugin_pane(window); }
|
||||||
|
void Plater::remove_plugin_panes()
|
||||||
|
{
|
||||||
|
while (!p->m_plugin_panes.empty())
|
||||||
|
p->remove_plugin_pane(p->m_plugin_panes.begin()->first);
|
||||||
|
}
|
||||||
|
void Plater::show_plugin_pane(wxWindow* window, bool show) { p->show_plugin_pane(window, show); }
|
||||||
|
|
||||||
//BBS
|
//BBS
|
||||||
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
|
||||||
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
|
||||||
|
|||||||
@@ -475,6 +475,17 @@ public:
|
|||||||
|
|
||||||
void reset_window_layout();
|
void reset_window_layout();
|
||||||
|
|
||||||
|
// Plugin panes dock alongside the sidebar; `window` must be a child of the Plater. `dock` is
|
||||||
|
// "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close
|
||||||
|
// button is destroyed after on_close runs; remove_plugin_pane() destroys it without calling on_close.
|
||||||
|
void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
|
||||||
|
const wxSize& size, std::function<void()> on_close);
|
||||||
|
void remove_plugin_pane(wxWindow* window);
|
||||||
|
void show_plugin_pane(wxWindow* window, bool show);
|
||||||
|
// Removes every plugin pane without calling on_close, for MainFrame::shutdown() (app exit and a
|
||||||
|
// language switch), while the Plater and any floating frames still exist.
|
||||||
|
void remove_plugin_panes();
|
||||||
|
|
||||||
// Called after the Preferences dialog is closed and the program settings are saved.
|
// Called after the Preferences dialog is closed and the program settings are saved.
|
||||||
// Update the UI based on the current preferences.
|
// Update the UI based on the current preferences.
|
||||||
void update_ui_from_settings();
|
void update_ui_from_settings();
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include "PluginDockPanel.hpp"
|
||||||
|
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "Plater.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title)
|
||||||
|
{
|
||||||
|
std::string name = "plugin:" + plugin_key + ":" + title;
|
||||||
|
std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_');
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name)
|
||||||
|
{
|
||||||
|
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
|
||||||
|
const std::string prefix = "name=" + pane_name + ";";
|
||||||
|
size_t begin = 0;
|
||||||
|
for (size_t i = 0; i <= layout.size(); ++i) {
|
||||||
|
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
|
||||||
|
continue;
|
||||||
|
if (layout.compare(begin, prefix.size(), prefix) == 0)
|
||||||
|
return layout.substr(begin, i - begin);
|
||||||
|
begin = i + 1;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginDockPanel::PluginDockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed)
|
||||||
|
: PluginWebPanel(parent, orca_bridge_script())
|
||||||
|
, m_html(html)
|
||||||
|
, m_on_message(std::move(on_message))
|
||||||
|
, m_on_close(std::move(on_close))
|
||||||
|
, m_on_destroyed(std::move(on_destroyed))
|
||||||
|
{
|
||||||
|
// A link asking for a new window has nowhere to open from a docked panel.
|
||||||
|
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginDockPanel::~PluginDockPanel()
|
||||||
|
{
|
||||||
|
if (m_on_destroyed)
|
||||||
|
m_on_destroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PluginDockPanel::on_page_message(const std::string& kind, const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (kind == "message") {
|
||||||
|
if (m_on_message)
|
||||||
|
m_on_message(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (kind == "close") {
|
||||||
|
request_close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::push_message(const nlohmann::json& data)
|
||||||
|
{
|
||||||
|
if (!m_closing)
|
||||||
|
post_to_page(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::fire_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
m_closing = true;
|
||||||
|
if (m_on_close) {
|
||||||
|
CloseHandler on_close = std::move(m_on_close);
|
||||||
|
m_on_close = nullptr;
|
||||||
|
on_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::request_close()
|
||||||
|
{
|
||||||
|
if (m_closing)
|
||||||
|
return;
|
||||||
|
fire_close();
|
||||||
|
// A close requested by the page arrives inside the web view's own script-message callback,
|
||||||
|
// which must return before the web view is destroyed.
|
||||||
|
CallAfter([this]() { remove_pane(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::destroy_for_plugin()
|
||||||
|
{
|
||||||
|
m_closing = true;
|
||||||
|
m_on_close = nullptr;
|
||||||
|
remove_pane();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginDockPanel::remove_pane()
|
||||||
|
{
|
||||||
|
if (Plater* plater = wxGetApp().plater())
|
||||||
|
plater->remove_plugin_pane(this);
|
||||||
|
else
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "PluginWebPanel.hpp"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// Name of a plugin's pane in the Plater's dock manager. It stays the same across sessions, so the
|
||||||
|
// saved window layout can put the pane back, and it never contains a wxAuiManager layout delimiter.
|
||||||
|
std::string plugin_pane_name(const std::string& plugin_key, const std::string& title);
|
||||||
|
|
||||||
|
// The pane part saved for `pane_name` in a wxAuiManager layout string, in the form
|
||||||
|
// wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
|
||||||
|
std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name);
|
||||||
|
|
||||||
|
// A PluginWebPanel docked in the Plater's dock manager, using the same window.orca bridge as
|
||||||
|
// PluginWebDialog (without submit, which only a plugin window handles). Python-agnostic for the same
|
||||||
|
// reason: it can be destroyed on the main thread without the GIL, so its hooks must not capture
|
||||||
|
// pybind11 objects.
|
||||||
|
class PluginDockPanel : public PluginWebPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using MessageHandler = std::function<void(const nlohmann::json& data)>;
|
||||||
|
using CloseHandler = std::function<void()>;
|
||||||
|
|
||||||
|
// on_close fires once, on a user or page initiated close. on_destroyed runs from the destructor
|
||||||
|
// on every path and must touch host-side state only.
|
||||||
|
PluginDockPanel(wxWindow* parent,
|
||||||
|
const std::string& html,
|
||||||
|
MessageHandler on_message,
|
||||||
|
CloseHandler on_close,
|
||||||
|
CloseHandler on_destroyed);
|
||||||
|
~PluginDockPanel() override;
|
||||||
|
|
||||||
|
// Main thread only.
|
||||||
|
void push_message(const nlohmann::json& data);
|
||||||
|
// Fires on_close, then removes the pane.
|
||||||
|
void request_close();
|
||||||
|
// Removes the pane without firing on_close, for plugin unload. Unlike request_close() it destroys
|
||||||
|
// the panel at once, which is safe because nothing a plugin page can call unloads a plugin: unload
|
||||||
|
// comes from the host (Plugins dialog, install, logout), never from this panel's own callbacks.
|
||||||
|
void destroy_for_plugin();
|
||||||
|
// Fires on_close at most once. The Plater calls it when the pane's own close button is used.
|
||||||
|
void fire_close();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::optional<std::string> page_html() override { return m_html; }
|
||||||
|
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void remove_pane();
|
||||||
|
|
||||||
|
std::string m_html;
|
||||||
|
bool m_closing{false};
|
||||||
|
MessageHandler m_on_message;
|
||||||
|
CloseHandler m_on_close;
|
||||||
|
CloseHandler m_on_destroyed;
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
#include "PluginWebDialog.hpp"
|
#include "PluginWebDialog.hpp"
|
||||||
|
|
||||||
#include "slic3r/GUI/GUI.hpp"
|
|
||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
|
#include "slic3r/GUI/PluginWebPanel.hpp"
|
||||||
#include <libslic3r/Utils.hpp>
|
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
|
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
|
|
||||||
@@ -13,50 +9,6 @@
|
|||||||
|
|
||||||
namespace Slic3r { namespace GUI {
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Injected into the top-level page at document start (before the plugin's own
|
|
||||||
// scripts). Defines window.orca as the only host surface the page may use. It
|
|
||||||
// references window.wx lazily (at call time) so it never races the backend's
|
|
||||||
// deferred registration of the "wx" message handler. Guarded against
|
|
||||||
// double-injection so it is harmless if also prepended.
|
|
||||||
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
|
||||||
(function () {
|
|
||||||
if (window.top !== window.self) return;
|
|
||||||
if (window.orca) return;
|
|
||||||
var handlers = [];
|
|
||||||
function send(kind, data) {
|
|
||||||
try {
|
|
||||||
window.wx.postMessage(JSON.stringify({
|
|
||||||
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
|
|
||||||
}));
|
|
||||||
} catch (e) { /* bridge not ready yet */ }
|
|
||||||
}
|
|
||||||
window.orca = {
|
|
||||||
postMessage: function (d) { send('message', d); },
|
|
||||||
submit: function (d) { send('submit', d); },
|
|
||||||
close: function () { send('close'); },
|
|
||||||
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
|
|
||||||
};
|
|
||||||
window.__orcaDispatch = function (payload) {
|
|
||||||
var data = payload ? payload.data : null;
|
|
||||||
for (var i = 0; i < handlers.length; i++) {
|
|
||||||
try { handlers[i](data); } catch (e) {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
)JS";
|
|
||||||
|
|
||||||
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
|
|
||||||
// relative URLs resolve against the bundled web resources directory.
|
|
||||||
wxString web_base_url()
|
|
||||||
{
|
|
||||||
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
|
||||||
return wxString("file://") + from_u8(dir) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
||||||
const wxString& title,
|
const wxString& title,
|
||||||
const std::string& html,
|
const std::string& html,
|
||||||
@@ -75,7 +27,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
|
|||||||
{
|
{
|
||||||
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
|
||||||
// is swapped in via SetPage once the bootstrap finishes loading.
|
// is swapped in via SetPage once the bootstrap finishes loading.
|
||||||
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
|
create_webview(PluginWebPanel::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
|
||||||
|
|
||||||
// Paint the window/webview in the themed background so there is no white
|
// Paint the window/webview in the themed background so there is no white
|
||||||
// flash before the (transparent) bootstrap page and plugin HTML render.
|
// flash before the (transparent) bootstrap page and plugin HTML render.
|
||||||
@@ -97,7 +49,7 @@ void PluginWebDialog::add_user_scripts()
|
|||||||
{
|
{
|
||||||
if (wxWebView* wv = browser()) {
|
if (wxWebView* wv = browser()) {
|
||||||
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||||
wv->AddUserScript(ORCA_BRIDGE_JS);
|
wv->AddUserScript(wxString::FromUTF8(PluginWebPanel::orca_bridge_script()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +103,7 @@ void PluginWebDialog::load_plugin_content()
|
|||||||
return;
|
return;
|
||||||
m_content_loaded = true;
|
m_content_loaded = true;
|
||||||
if (wxWebView* wv = browser())
|
if (wxWebView* wv = browser())
|
||||||
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
|
wv->SetPage(wxString::FromUTF8(m_html), PluginWebPanel::content_base_url());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "PluginWebPanel.hpp"
|
||||||
|
|
||||||
|
#include "GUI.hpp"
|
||||||
|
#include "GUI_App.hpp"
|
||||||
|
#include "Widgets/WebView.hpp"
|
||||||
|
#include "Widgets/WebViewHostDialog.hpp"
|
||||||
|
|
||||||
|
#include <libslic3r/Utils.hpp>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
#include <wx/sizer.h>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Injected into the top-level page at document start (before the plugin's own
|
||||||
|
// scripts). Defines window.orca as the only host surface the page may use. It
|
||||||
|
// references window.wx lazily (at call time) so it never races the backend's
|
||||||
|
// deferred registration of the "wx" message handler. Guarded against
|
||||||
|
// double-injection so it is harmless if also prepended.
|
||||||
|
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
||||||
|
(function () {
|
||||||
|
if (window.top !== window.self) return;
|
||||||
|
if (window.orca) return;
|
||||||
|
var handlers = [];
|
||||||
|
function send(kind, data) {
|
||||||
|
try {
|
||||||
|
window.wx.postMessage(JSON.stringify({
|
||||||
|
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
|
||||||
|
}));
|
||||||
|
} catch (e) { /* bridge not ready yet */ }
|
||||||
|
}
|
||||||
|
window.orca = {
|
||||||
|
postMessage: function (d) { send('message', d); },
|
||||||
|
submit: function (d) { send('submit', d); },
|
||||||
|
close: function () { send('close'); },
|
||||||
|
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
|
||||||
|
};
|
||||||
|
window.__orcaDispatch = function (payload) {
|
||||||
|
var data = payload ? payload.data : null;
|
||||||
|
for (var i = 0; i < handlers.length; i++) {
|
||||||
|
try { handlers[i](data); } catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
)JS";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
wxString PluginWebPanel::content_base_url()
|
||||||
|
{
|
||||||
|
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
||||||
|
return wxString("file://") + from_u8(dir) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* PluginWebPanel::orca_bridge_script() { return ORCA_BRIDGE_JS; }
|
||||||
|
|
||||||
|
PluginWebPanel::PluginWebPanel(wxWindow* parent, const char* bridge_script)
|
||||||
|
: wxPanel(parent, wxID_ANY)
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
SetSizer(sizer);
|
||||||
|
|
||||||
|
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
|
||||||
|
const std::string bootstrap = (boost::filesystem::path(resources_dir()) / BOOTSTRAP_PAGE).make_preferred().string();
|
||||||
|
m_browser = WebView::CreateWebView(this, wxString("file://") + from_u8(bootstrap));
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
|
||||||
|
m_browser->AddUserScript(wxString::FromUTF8(bridge_script));
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginWebPanel::on_load_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginWebPanel::on_load_event, this);
|
||||||
|
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginWebPanel::on_script_message, this);
|
||||||
|
m_browser->Bind(EVT_WEBVIEW_RECREATED, &PluginWebPanel::on_webview_recreated, this);
|
||||||
|
sizer->Add(m_browser, 1, wxEXPAND);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginWebPanel::on_load_event(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
if (!m_content_loaded) {
|
||||||
|
// The first bootstrap load (or its error) triggers the swap to the plugin HTML.
|
||||||
|
m_content_loaded = true;
|
||||||
|
if (const std::optional<std::string> html = page_html())
|
||||||
|
m_browser->SetPage(wxString::FromUTF8(*html), content_base_url());
|
||||||
|
} else if (event.GetEventType() == wxEVT_WEBVIEW_LOADED) {
|
||||||
|
// The document-start theme script keeps the theme the web view was created with, so bring
|
||||||
|
// every later document (the plugin page, or one a link loaded) onto the current app theme.
|
||||||
|
apply_theme();
|
||||||
|
}
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginWebPanel::on_script_message(wxWebViewEvent& event)
|
||||||
|
{
|
||||||
|
const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false);
|
||||||
|
if (!payload.is_object() || payload.value("channel", std::string()) != "orca")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::string kind = payload.value("kind", std::string());
|
||||||
|
if (!on_page_message(kind, payload.contains("data") ? payload["data"] : nlohmann::json()))
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Plugin web panel ignored a window.orca '" << kind << "' call; this host does not support it";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginWebPanel::on_webview_recreated(wxCommandEvent&)
|
||||||
|
{
|
||||||
|
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||||
|
m_browser->SetBackgroundColour(GetBackgroundColour());
|
||||||
|
Refresh();
|
||||||
|
// Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page.
|
||||||
|
apply_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginWebPanel::apply_theme()
|
||||||
|
{
|
||||||
|
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PluginWebPanel::post_to_page(const std::string& json)
|
||||||
|
{
|
||||||
|
WebView::RunScript(m_browser, wxString::Format(
|
||||||
|
"(function dispatch(payload, attempts) {\n"
|
||||||
|
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
||||||
|
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
||||||
|
"})({data: %s}, 0);",
|
||||||
|
wxString::FromUTF8(json)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <wx/panel.h>
|
||||||
|
#include <wx/string.h>
|
||||||
|
#include <wx/webview.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r { namespace GUI {
|
||||||
|
|
||||||
|
// A panel hosting plugin-supplied HTML in a web view, shared by plugin Pages tabs and docked panels.
|
||||||
|
// It loads the bundled bootstrap page, swaps the plugin HTML in once that has loaded, registers the
|
||||||
|
// host theme, the plugin element defaults and the given window.orca bridge, keeps the page on the
|
||||||
|
// app theme, and routes the bridge's messages to on_page_message().
|
||||||
|
class PluginWebPanel : public wxPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PluginWebPanel(wxWindow* parent, const char* bridge_script);
|
||||||
|
|
||||||
|
// Shared by every plugin web view, PluginWebDialog included.
|
||||||
|
// The bundled blank page a plugin web view loads before the plugin HTML is swapped in.
|
||||||
|
static constexpr const char* BOOTSTRAP_PAGE = "web/dialog/PluginWebDialog/blank.html";
|
||||||
|
// The file:// base URL plugin HTML is loaded against, so relative URLs resolve against the
|
||||||
|
// bundled web resources.
|
||||||
|
static wxString content_base_url();
|
||||||
|
// The window.orca bridge of plugin windows and docked panels: postMessage, submit, close and
|
||||||
|
// onMessage. Each host acts only on the message kinds it supports.
|
||||||
|
static const char* orca_bridge_script();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|
||||||
|
// Delivers an already serialised JSON value to the page's window.orca.onMessage handlers,
|
||||||
|
// waiting briefly for the bridge while the page is still loading. Main thread only.
|
||||||
|
void post_to_page(const std::string& json);
|
||||||
|
|
||||||
|
// The plugin HTML to show once the bootstrap page has loaded; std::nullopt leaves it blank.
|
||||||
|
virtual std::optional<std::string> page_html() = 0;
|
||||||
|
// A window.orca message from the page. Returns false for a kind this host does not handle,
|
||||||
|
// which is logged so a dropped call is diagnosable.
|
||||||
|
virtual bool on_page_message(const std::string& kind, const nlohmann::json& data) = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void on_load_event(wxWebViewEvent& event);
|
||||||
|
void on_script_message(wxWebViewEvent& event);
|
||||||
|
void on_webview_recreated(wxCommandEvent& event);
|
||||||
|
void apply_theme();
|
||||||
|
|
||||||
|
wxWebView* m_browser{nullptr};
|
||||||
|
bool m_content_loaded{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
}} // namespace Slic3r::GUI
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ if(document.documentElement)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); }
|
||||||
|
|
||||||
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
|
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
|
||||||
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
|
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
|
||||||
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
|
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public:
|
|||||||
// Shared by modeless Pages tabs and PluginWebDialog.
|
// Shared by modeless Pages tabs and PluginWebDialog.
|
||||||
static std::string theme_user_script();
|
static std::string theme_user_script();
|
||||||
static std::string plugin_defaults_user_script();
|
static std::string plugin_defaults_user_script();
|
||||||
|
// Re-themes an already-loaded page in place, for web views hosted outside a dialog.
|
||||||
|
static std::string theme_apply_script();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
wxWebView* browser() const { return m_browser; }
|
wxWebView* browser() const { return m_browser; }
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <slic3r/GUI/GUI_App.hpp>
|
#include <slic3r/GUI/GUI_App.hpp>
|
||||||
#include <slic3r/GUI/MainFrame.hpp>
|
#include <slic3r/GUI/MainFrame.hpp>
|
||||||
#include <slic3r/GUI/MsgDialog.hpp>
|
#include <slic3r/GUI/MsgDialog.hpp>
|
||||||
|
#include <slic3r/GUI/Plater.hpp>
|
||||||
|
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||||
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
#include <slic3r/GUI/PluginProgressDialog.hpp>
|
||||||
#include <slic3r/GUI/PluginWebDialog.hpp>
|
#include <slic3r/GUI/PluginWebDialog.hpp>
|
||||||
#include <slic3r/GUI/NotificationManager.hpp>
|
#include <slic3r/GUI/NotificationManager.hpp>
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
#include <wx/defs.h>
|
#include <wx/defs.h>
|
||||||
#include <wx/window.h>
|
#include <wx/window.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <future>
|
#include <future>
|
||||||
@@ -109,6 +112,25 @@ GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The plugin's on_close: fired only on a user/JS-initiated close (not forced teardown), while the
|
||||||
|
// window is alive. Empty if the plugin passed None.
|
||||||
|
std::function<void()> make_close_adapter(const CallablePtr& holder)
|
||||||
|
{
|
||||||
|
if (!holder)
|
||||||
|
return nullptr;
|
||||||
|
return [holder]() {
|
||||||
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
holder->fn();
|
||||||
|
} catch (py::error_already_set& e) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
|
||||||
// owning plugin so all of a plugin's UI can be torn down on unload.
|
// owning plugin so all of a plugin's UI can be torn down on unload.
|
||||||
@@ -350,22 +372,7 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
if (!UiRegistry::instance().is_open(new_id))
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
|
GUI::PluginWebDialog::CloseHandler on_close = make_close_adapter(close_holder);
|
||||||
// teardown), while the dialog is alive. Empty if the plugin passed None.
|
|
||||||
GUI::PluginWebDialog::CloseHandler on_close;
|
|
||||||
if (close_holder) {
|
|
||||||
on_close = [close_holder]() {
|
|
||||||
PythonGILState gil;
|
|
||||||
if (!gil)
|
|
||||||
return;
|
|
||||||
try {
|
|
||||||
close_holder->fn();
|
|
||||||
} catch (py::error_already_set& e) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
|
||||||
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
|
||||||
@@ -387,14 +394,69 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
|
|||||||
return py::cast(UiWindowHandle{new_id});
|
return py::cast(UiWindowHandle{new_id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// orca.host.ui.create_dock_panel + UiDockPanel handle
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
constexpr const char* DOCK_POSITIONS[] = {"left", "right", "bottom", "float"};
|
||||||
|
|
||||||
|
struct UiDockPanelHandle
|
||||||
|
{
|
||||||
|
int id{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
py::object ui_create_dock_panel(const std::string& html, const std::string& title, int width, int height,
|
||||||
|
py::object on_message, py::object on_close, const std::string& dock)
|
||||||
|
{
|
||||||
|
if (std::find(std::begin(DOCK_POSITIONS), std::end(DOCK_POSITIONS), dock) == std::end(DOCK_POSITIONS))
|
||||||
|
throw std::invalid_argument("orca.host.ui.create_dock_panel dock must be \"left\", \"right\", \"bottom\" or \"float\"");
|
||||||
|
|
||||||
|
auto msg_adapter = make_message_adapter(std::move(on_message));
|
||||||
|
CallablePtr close_holder = make_holder(std::move(on_close));
|
||||||
|
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||||
|
const int w = width > 0 ? width : 320;
|
||||||
|
const int h = height > 0 ? height : 480;
|
||||||
|
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||||
|
|
||||||
|
// Deferred and pre-bound for the same reasons as create_window().
|
||||||
|
const int new_id = UiRegistry::instance().reserve_id();
|
||||||
|
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
|
||||||
|
|
||||||
|
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
|
||||||
|
msg_adapter = std::move(msg_adapter),
|
||||||
|
close_holder = std::move(close_holder)]() mutable {
|
||||||
|
if (!UiRegistry::instance().is_open(new_id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
|
||||||
|
UiRegistry::instance().remove(new_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
|
||||||
|
auto* panel = new GUI::PluginDockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
|
||||||
|
std::move(on_destroyed));
|
||||||
|
UiRegistry::instance().bind(new_id, panel, plugin_key);
|
||||||
|
plater->add_plugin_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
|
||||||
|
wxSize(w, h), [panel]() { panel->fire_close(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
return py::cast(UiDockPanelHandle{new_id});
|
||||||
|
}
|
||||||
|
|
||||||
void handle_post(int id, py::object data)
|
void handle_post(int id, py::object data)
|
||||||
{
|
{
|
||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
json j = py_to_json(data); // GIL held (binding body)
|
json j = py_to_json(data); // GIL held (binding body)
|
||||||
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::post_message(d, j);
|
if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->push_message(j);
|
||||||
|
else
|
||||||
|
GUI::PluginWebDialog::post_message(dynamic_cast<GUI::PluginWebDialog*>(window), j);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +465,23 @@ void handle_close(int id)
|
|||||||
if (wxTheApp == nullptr)
|
if (wxTheApp == nullptr)
|
||||||
return;
|
return;
|
||||||
GUI::wxGetApp().CallAfter([id]() {
|
GUI::wxGetApp().CallAfter([id]() {
|
||||||
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
|
auto* window = UiRegistry::instance().get_as<wxWindow>(id);
|
||||||
GUI::PluginWebDialog::request_close(d);
|
if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->request_close();
|
||||||
|
else
|
||||||
|
GUI::PluginWebDialog::request_close(dynamic_cast<GUI::PluginWebDialog*>(window));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_show(int id, bool show)
|
||||||
|
{
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
return;
|
||||||
|
GUI::wxGetApp().CallAfter([id, show]() {
|
||||||
|
auto* panel = UiRegistry::instance().get_as<GUI::PluginDockPanel>(id);
|
||||||
|
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||||
|
if (panel != nullptr && plater != nullptr)
|
||||||
|
plater->show_plugin_pane(panel, show);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,6 +640,31 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
|||||||
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
"or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) "
|
||||||
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
"is called when the page submits; offload heavy work to a thread and push results back with window.post().");
|
||||||
|
|
||||||
|
py::class_<UiDockPanelHandle>(ui, "UiDockPanel", "Handle to a dockable plugin HTML panel created by create_dock_panel().")
|
||||||
|
.def_property_readonly("id", [](const UiDockPanelHandle& h) { return h.id; })
|
||||||
|
.def(
|
||||||
|
"post", [](const UiDockPanelHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
|
||||||
|
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
|
||||||
|
.def(
|
||||||
|
"show", [](const UiDockPanelHandle& h) { handle_show(h.id, true); }, "Show the panel again after hide().")
|
||||||
|
.def(
|
||||||
|
"hide", [](const UiDockPanelHandle& h) { handle_show(h.id, false); }, "Hide the panel without closing it.")
|
||||||
|
.def(
|
||||||
|
"close", [](const UiDockPanelHandle& h) { handle_close(h.id); }, "Close the panel (fires on_close).")
|
||||||
|
.def(
|
||||||
|
"is_open", [](const UiDockPanelHandle& h) { return UiRegistry::instance().is_open(h.id); },
|
||||||
|
"Return True until the panel is closed; a hidden panel is still open.");
|
||||||
|
|
||||||
|
ui.def("create_dock_panel", &ui_create_dock_panel, py::arg("html"), py::arg("title") = "OrcaSlicer",
|
||||||
|
py::arg("width") = 320, py::arg("height") = 480, py::arg("on_message") = py::none(),
|
||||||
|
py::arg("on_close") = py::none(), py::arg("dock") = "right",
|
||||||
|
"Open an HTML panel docked beside the 3D view and return a UiDockPanel. dock is \"left\", \"right\", "
|
||||||
|
"\"bottom\" or \"float\", and width/height are in DIPs; the user can move and resize the panel, and the "
|
||||||
|
"saved window layout remembers both. The panel belongs to the Prepare and Preview tabs. on_message(data) "
|
||||||
|
"is called on the UI thread when the page posts; window.orca.close() or the panel's close button closes "
|
||||||
|
"it and calls on_close(). A post() made before the page has loaded can be dropped, so have the page "
|
||||||
|
"request its first data.");
|
||||||
|
|
||||||
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
|
||||||
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
|
||||||
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
|
||||||
@@ -632,6 +734,8 @@ void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
|
|||||||
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
|
||||||
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
if (auto* dialog = dynamic_cast<GUI::PluginWebDialog*>(window))
|
||||||
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
GUI::PluginWebDialog::destroy_for_plugin(dialog);
|
||||||
|
else if (auto* panel = dynamic_cast<GUI::PluginDockPanel*>(window))
|
||||||
|
panel->destroy_for_plugin();
|
||||||
else if (window != nullptr)
|
else if (window != nullptr)
|
||||||
window->Destroy();
|
window->Destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
#include "PluginPages.hpp"
|
#include "PluginPages.hpp"
|
||||||
|
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
#include "slic3r/GUI/GUI.hpp"
|
|
||||||
#include "slic3r/GUI/Notebook.hpp"
|
#include "slic3r/GUI/Notebook.hpp"
|
||||||
#include "slic3r/GUI/GUI_App.hpp"
|
#include "slic3r/GUI/GUI_App.hpp"
|
||||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||||
#include "slic3r/GUI/Widgets/WebView.hpp"
|
|
||||||
#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp"
|
|
||||||
#include "slic3r/GUI/wxExtensions.hpp"
|
#include "slic3r/GUI/wxExtensions.hpp"
|
||||||
#include "slic3r/plugin/PluginManager.hpp"
|
#include "slic3r/plugin/PluginManager.hpp"
|
||||||
|
|
||||||
#include <libslic3r/Utils.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
@@ -66,27 +61,11 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
|
||||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
|
: GUI::PluginWebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
|
||||||
, m_cap(std::move(capability))
|
, m_cap(std::move(capability))
|
||||||
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
|
||||||
{
|
{
|
||||||
auto* topsizer = new wxBoxSizer(wxVERTICAL);
|
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
||||||
SetSizer(topsizer);
|
|
||||||
|
|
||||||
m_browser = WebView::CreateWebView(this, bootstrap_url());
|
|
||||||
if (m_browser == nullptr) {
|
|
||||||
wxLogError("Could not initialize plugin page web view");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
|
|
||||||
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this);
|
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script()));
|
|
||||||
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script()));
|
|
||||||
m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS);
|
|
||||||
|
|
||||||
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
|
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
|
||||||
m_cap->set_message_sender([lifetime](const std::string& message) {
|
m_cap->set_message_sender([lifetime](const std::string& message) {
|
||||||
@@ -117,89 +96,54 @@ void PluginPage::detach_capability()
|
|||||||
m_cap.reset();
|
m_cap.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
wxString PluginPage::web_base_url() const
|
std::optional<std::string> PluginPage::page_html()
|
||||||
{
|
{
|
||||||
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
|
if (m_cap == nullptr)
|
||||||
return wxString("file://") + GUI::from_u8(path) + "/";
|
return std::nullopt;
|
||||||
}
|
|
||||||
|
|
||||||
wxString PluginPage::bootstrap_url() const
|
|
||||||
{
|
|
||||||
const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string();
|
|
||||||
return wxString("file://") + GUI::from_u8(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PluginPage::on_bootstrap_event(wxWebViewEvent& event)
|
|
||||||
{
|
|
||||||
load_plugin_content();
|
|
||||||
event.Skip();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PluginPage::load_plugin_content()
|
|
||||||
{
|
|
||||||
if (m_content_loaded || m_browser == nullptr || m_cap == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_content_loaded = true;
|
|
||||||
try {
|
try {
|
||||||
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
|
return m_cap->get_ui();
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
|
||||||
detach_capability();
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
|
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
|
||||||
detach_capability();
|
|
||||||
}
|
}
|
||||||
|
detach_capability();
|
||||||
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::on_new_window(wxWebViewEvent& event)
|
void PluginPage::on_new_window(wxWebViewEvent& event)
|
||||||
{
|
{
|
||||||
const wxString url = event.GetURL();
|
const wxString url = event.GetURL();
|
||||||
if (!url.empty() && m_browser != nullptr)
|
if (!url.empty())
|
||||||
m_browser->LoadURL(url);
|
browser()->LoadURL(url);
|
||||||
event.Veto();
|
event.Veto();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::on_script_message(wxWebViewEvent& event)
|
bool PluginPage::on_page_message(const std::string& kind, const nlohmann::json& data)
|
||||||
{
|
{
|
||||||
|
if (kind != "message")
|
||||||
|
return false;
|
||||||
if (!m_cap)
|
if (!m_cap)
|
||||||
return;
|
return true;
|
||||||
|
|
||||||
const wxString payload = event.GetString();
|
|
||||||
nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false);
|
|
||||||
if (root.is_discarded() || root.value("channel", std::string()) != "orca" ||
|
|
||||||
root.value("kind", std::string()) != "message")
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto data = root.find("data");
|
|
||||||
try {
|
try {
|
||||||
m_cap->on_message(data == root.end()
|
m_cap->on_message(data.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
? "null"
|
|
||||||
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception& error) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginPage::push_message(const std::string& message)
|
void PluginPage::push_message(const std::string& message)
|
||||||
{
|
{
|
||||||
if (m_browser == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
|
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
|
||||||
// non-JSON payload needs wrapping as a string literal.
|
// non-JSON payload needs wrapping as a string literal.
|
||||||
const std::string payload = nlohmann::json::accept(message)
|
post_to_page(nlohmann::json::accept(message)
|
||||||
? message
|
? message
|
||||||
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
|
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
|
||||||
|
|
||||||
WebView::RunScript(m_browser, wxString::Format(
|
|
||||||
"(function dispatch(payload, attempts) {\n"
|
|
||||||
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
|
|
||||||
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
|
|
||||||
"})({data: %s}, 0);",
|
|
||||||
wxString::FromUTF8(payload)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginPages::~PluginPages()
|
PluginPages::~PluginPages()
|
||||||
@@ -268,10 +212,6 @@ bool PluginPages::create_page(const PluginCapabilityId& id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto* page = new PluginPage(m_parent, std::move(capability));
|
auto* page = new PluginPage(m_parent, std::move(capability));
|
||||||
if (!page->is_valid()) {
|
|
||||||
page->Destroy();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!icon.empty()) {
|
if (!icon.empty()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <slic3r/GUI/PluginWebPanel.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
|
||||||
|
|
||||||
@@ -11,14 +12,13 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <wx/bitmap.h>
|
#include <wx/bitmap.h>
|
||||||
#include <wx/panel.h>
|
|
||||||
#include <wx/webview.h>
|
#include <wx/webview.h>
|
||||||
|
|
||||||
class Notebook;
|
class Notebook;
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
class PluginPage : public wxPanel
|
class PluginPage : public GUI::PluginWebPanel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
|
||||||
@@ -26,24 +26,20 @@ public:
|
|||||||
|
|
||||||
PluginPage() = delete;
|
PluginPage() = delete;
|
||||||
|
|
||||||
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
|
|
||||||
void detach_capability();
|
void detach_capability();
|
||||||
void on_bootstrap_event(wxWebViewEvent& event);
|
|
||||||
void on_new_window(wxWebViewEvent& event);
|
|
||||||
void on_script_message(wxWebViewEvent& event);
|
|
||||||
void push_message(const std::string& message);
|
void push_message(const std::string& message);
|
||||||
void set_icon(const wxBitmap& icon) { m_icon = icon; }
|
void set_icon(const wxBitmap& icon) { m_icon = icon; }
|
||||||
const wxBitmap& icon() const { return m_icon; }
|
const wxBitmap& icon() const { return m_icon; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::optional<std::string> page_html() override;
|
||||||
|
bool on_page_message(const std::string& kind, const nlohmann::json& data) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load_plugin_content();
|
void on_new_window(wxWebViewEvent& event);
|
||||||
wxString bootstrap_url() const;
|
|
||||||
wxString web_base_url() const;
|
|
||||||
|
|
||||||
wxWebView* m_browser{nullptr};
|
|
||||||
std::shared_ptr<PagesPluginCapability> m_cap;
|
std::shared_ptr<PagesPluginCapability> m_cap;
|
||||||
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
|
||||||
bool m_content_loaded{false};
|
|
||||||
wxBitmap m_icon;
|
wxBitmap m_icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <libslic3r/Model.hpp>
|
#include <libslic3r/Model.hpp>
|
||||||
#include <libslic3r/PresetBundle.hpp>
|
#include <libslic3r/PresetBundle.hpp>
|
||||||
#include <libslic3r/TriangleMesh.hpp>
|
#include <libslic3r/TriangleMesh.hpp>
|
||||||
|
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||||
|
|
||||||
#include "python_test_support.hpp"
|
#include "python_test_support.hpp"
|
||||||
@@ -141,6 +142,8 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
||||||
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
||||||
CHECK(has_attr(ui, "UiWindow"));
|
CHECK(has_attr(ui, "UiWindow"));
|
||||||
|
CHECK(has_attr(ui, "create_dock_panel"));
|
||||||
|
CHECK(has_attr(ui, "UiDockPanel"));
|
||||||
|
|
||||||
// With no wx application the UI calls marshal to a main thread that does not
|
// With no wx application the UI calls marshal to a main thread that does not
|
||||||
// exist here; they must fail cleanly with a clear error, not crash.
|
// exist here; they must fail cleanly with a clear error, not crash.
|
||||||
@@ -151,6 +154,53 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
|||||||
CHECK(error.matches(PyExc_RuntimeError));
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
|
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positional arguments follow create_window(): width and height come straight after the title.
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>", "Panel", 400, 300);
|
||||||
|
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_RuntimeError));
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unknown dock position is rejected before the application is needed.
|
||||||
|
try {
|
||||||
|
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
||||||
|
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
||||||
|
} catch (const py::error_already_set& error) {
|
||||||
|
CHECK(error.matches(PyExc_ValueError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::plugin_pane_name;
|
||||||
|
|
||||||
|
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
||||||
|
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A plugin pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||||
|
{
|
||||||
|
using Slic3r::GUI::plugin_pane_layout_entry;
|
||||||
|
|
||||||
|
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
||||||
|
// The caption holds an escaped '|', which must not end the entry.
|
||||||
|
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
||||||
|
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
||||||
|
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||||
|
CHECK(plugin_pane_layout_entry(layout, "plugin:demo").empty());
|
||||||
|
CHECK(plugin_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||||
|
|||||||
Reference in New Issue
Block a user