Merge branch 'main' into feat/preset-plugin-apis

This commit is contained in:
peachismomo committed 2026-10-16 21:59:48 +08:00
commit 3bfe40e17a
63 files changed
+2975 -386

No files matched your search

+21 -15
View File
@@ -163,6 +163,7 @@ using json = nlohmann::json;
#include <X11/Xlib.h>
#endif
#include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif
#ifdef SLIC3R_GUI
@@ -1156,8 +1157,10 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on
// XWayland sessions as a conservative fallback for older WebKit.
// Wizard / login / store. The default path applies it only where it
// forces GDK_BACKEND=x11 below (a build without EGL, so the app really
// runs through XWayland), and only when the linked WebKitGTK predates
// the fix (< 2.42), as a conservative fallback for older WebKit.
// ------------------------------------------------------------------
{
const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1165,6 +1168,14 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list).
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
// Force the shared-memory transport, which keeps compositing and threaded
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1204,23 +1215,18 @@ int CLI::run(int argc, char **argv)
if (wayland_env && *wayland_env) {
BOOST_LOG_TRIVIAL(warning) << "Wayland detected but wxWidgets has no EGL support (wxHAS_EGL is OFF). Forcing X11 backend.";
::setenv("GDK_BACKEND", "x11", true);
// GTK will use X11 in a Wayland session here, which is the
// one default-path case that really runs through XWayland.
// WebKit2GTK accelerated compositing could fail silently
// there on releases before 2.42, so force it off. Disabling
// it also turns off threaded scrolling, hence the gate.
if (WebView::NeedsCompositingWorkaround())
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
#endif
// WebKit2GTK compositing can fail under XWayland on older
// WebKit releases. Disable it only when both DISPLAY and
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in
// use). On pure X11 or native Wayland, compositing is left
// enabled so WebKit retains HW acceleration.
{
const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
}
}
// XInitThreads is needed before GStreamer may use Xlib. On
// native Wayland without DISPLAY, GStreamer uses waylandsink
// (no Xlib involved), so the call is skipped.
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink
export WEBKIT_DISABLE_DMABUF_RENDERER=1
# WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
fi
fi
fi
+27 -18
View File
@@ -432,27 +432,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string OozePrevention::pre_toolchange(GCode& gcodegen)
{
std::string gcode;
unsigned int extruder_id = gcodegen.writer().filament()->id();
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
// There is no idle temperature defined in filament settings.
// Use the delta value from print config.
if (gcodegen.config().standby_temperature_delta.value != 0) {
// we assume that heating is always slower than cooling, so no need to block
gcode += gcodegen.writer().set_temperature
(this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
}
} else {
// Use the value from filament settings. That one is absolute, not delta.
gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
// Check if this tool will ever be used again in this print.
bool is_last_use = false;
if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) {
coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z();
const auto& tool_ordering = gcodegen.m_print->tool_ordering();
const auto& cfg = gcodegen.config();
const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id));
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id);
for (unsigned int f : tool_ordering.all_extruders())
if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater)
is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f);
}
int temp = 0;
if (!is_last_use) {
const auto& filament_idle_temp = gcodegen.config().idle_temperature;
if (filament_idle_temp.get_at(extruder_id) == 0) {
if (gcodegen.config().standby_temperature_delta.value == 0)
return std::string();
temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value;
} else {
temp = filament_idle_temp.get_at(extruder_id);
}
}
std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id);
gcode.pop_back();
gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed
return gcode;
}
+1
View File
@@ -1004,6 +1004,7 @@ protected:
size_t num_objects,
size_t num_islands);
friend class OozePrevention;
friend class Wipe;
friend class WipeTowerIntegration;
friend class PressureEqualizer;
+14 -1
View File
@@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material)
sort_remove_duplicates(m_all_printing_extruders);
}
m_last_layer_per_extruder.clear();
for (const LayerTools& lt : m_layer_tools) {
for (unsigned int ext : lt.extruders) {
m_last_layer_per_extruder[ext] = lt.print_z;
}
}
if (prime_multi_material && ! m_all_printing_extruders.empty()) {
// Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder.
// Then set m_first_printing_extruder to the 1st extruder primed.
@@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
return -1;
}
bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const
{
auto it = m_last_layer_per_extruder.find(extruder_id);
if (it == m_last_layer_per_extruder.end())
return true;
return print_z >= it->second - EPSILON;
}
} // namespace Slic3r
+3
View File
@@ -370,6 +370,7 @@ public:
std::vector<int> & initial_filaments);
bool has_non_support_filament(const PrintConfig &config);
bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const;
private:
void initialize_layers(std::vector<coordf_t> &zs);
@@ -413,6 +414,8 @@ private:
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
// no-ops; downstream emission falls through to the per-object branch).
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+29
View File
@@ -211,6 +211,35 @@ bool decode_colored_png(const ReadBuf &in_buf, ImageColorscale &out_img)
return decode_colored_png(stream, out_img);
}
bool is_decodable_8bit_png(const ReadBuf &pngbuf)
{
static const constexpr int PNG_SIG_BYTES = 8;
struct ReadBufStream stream{pngbuf};
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
stream.read(sig.data(), PNG_SIG_BYTES);
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
return false;
PNGDescr dsc;
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!dsc.png)
return false;
dsc.info = png_create_info_struct(dsc.png);
if (!dsc.info)
return false;
if (!png_read_header_guarded(dsc.png, dsc.info, &stream, PNG_SIG_BYTES))
return false;
const size_t color_type = png_get_color_type(dsc.png, dsc.info);
const size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
const bool grey = color_type == PNG_COLOR_TYPE_GRAY;
const bool colour = color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA;
return png_get_image_width(dsc.png, dsc.info) > 0 && png_get_image_height(dsc.png, dsc.info) > 0 &&
bit_depth == 8 && (grey || colour);
}
// Down to earth function to store a packed RGB image to file. Mostly useful for debugging purposes.
// Based on https://www.lemoda.net/c/write-png/
+3
View File
@@ -50,6 +50,9 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
bool is_png(const ReadBuf &pngbuf);
// True if decode_png() or decode_colored_png() would accept these bytes, judged from the header alone.
bool is_decodable_8bit_png(const ReadBuf &pngbuf);
struct ReadBufStream: public IStream {
const ReadBuf &rbuf_ref;
size_t pos = 0;
+10 -8
View File
@@ -283,7 +283,7 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded)
{
DecodedHeightTexture result;
if (layer.empty())
@@ -351,13 +351,15 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
}
}
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
auto &entries = g_decoded_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();)
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
if (keep_decoded) {
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
// Opportunistically drop entries for image_data that no longer exists anywhere, so the
// cache doesn't grow without bound across many add/remove-texture cycles in a long session.
auto &entries = g_decoded_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();)
it = it->second.first.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
}
}
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
+3 -2
View File
@@ -599,8 +599,9 @@ DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, i
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
// as it did when the importer flattened it to grey on the way in. Returns an empty
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// DecodedHeightTexture if image_data is empty or is not a PNG at all. keep_decoded = false leaves the
// result out of the decode cache, for one-off reads such as a picker thumbnail that must not hold full-size pixels.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer, bool keep_decoded = true);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
+7
View File
@@ -842,6 +842,13 @@ void load_string_file(const boost::filesystem::path& p, std::string& str);
bool check_layer_id_pattern(const std::string& pattern, int layer_id);
// On Windows, return an equivalent path carrying the extended-length ("\\?\" / "\\?\UNC\")
// prefix so the Win32 APIs accept it past MAX_PATH even when LongPathsEnabled is off. The path
// is made absolute and lexically normalized first, because the prefix disables normalisation.
// A path that is already extended (or a device path) is returned unchanged; on other platforms,
// or when an absolute path cannot be formed, the input is returned unchanged.
boost::filesystem::path extended_path(const boost::filesystem::path& path);
} // namespace Slic3r
#if WIN32
+53 -8
View File
@@ -17,6 +17,7 @@
#if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp"
#include "boost/nowide/convert.hpp"
#endif
#include "I18N.hpp"
@@ -62,7 +63,11 @@ bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
FILE *f = nullptr;
#if defined(_MSC_VER) || defined(__MINGW64__)
f = boost::nowide::fopen(fname, mode);
// fname is UTF-8 (the nowide contract). Route it through the shared extended-path helper so an
// archive stored past MAX_PATH opens even with LongPathsEnabled off.
const std::wstring wide_path = boost::nowide::widen(fname);
const std::string extended = boost::nowide::narrow(Slic3r::extended_path(boost::filesystem::path(wide_path)).wstring());
f = boost::nowide::fopen(extended.c_str(), mode);
#elif defined(__GNUC__) && defined(_LARGEFILE64_SOURCE)
f = fopen64(fname, mode);
#else
@@ -101,6 +106,14 @@ bool close_zip(mz_zip_archive *zip, bool isread)
}
return ret;
}
#ifdef WIN32
// mz_file_write_func writing an extracted entry into an already-open FILE*.
size_t mz_write_to_cfile(void *opaque, mz_uint64 /*file_ofs*/, const void *buffer, size_t size)
{
return std::fwrite(buffer, 1, size, static_cast<FILE *>(opaque));
}
#endif
}
bool open_zip_reader(mz_zip_archive *zip, const std::string &fname)
@@ -116,6 +129,36 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path)
{
if (!zip)
return false;
#ifdef WIN32
// Open the destination ourselves with a Windows extended-length path: miniz's narrow
// extract_to_file cannot be relied on past MAX_PATH with LongPathsEnabled off, and its wide
// variant takes a plain (unprefixed) path and a text-mode stream. This keeps the write binary.
FILE *file = ::_wfopen(Slic3r::extended_path(dest_path).wstring().c_str(), L"wb");
if (!file) {
zip->m_last_error = MZ_ZIP_FILE_OPEN_FAILED;
return false;
}
const bool written = mz_zip_reader_extract_to_callback(zip, file_index, mz_write_to_cfile, file, 0) != 0;
const bool closed = std::fclose(file) == 0;
if (!written || !closed) {
if (written && !closed)
zip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
// Drop the partial entry rather than leave a truncated file behind.
boost::system::error_code ec;
boost::filesystem::remove(Slic3r::extended_path(dest_path), ec);
return false;
}
return true;
#else
return mz_zip_reader_extract_to_file(zip, file_index, dest_path.string().c_str(), 0) != 0;
#endif
}
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
{
if (stat.m_is_utf8)
@@ -156,10 +199,13 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
}
const std::string dest_file = dest_dir + "/" + stat.m_filename;
try {
// Use the extended-length form for every filesystem call so entries under a destination
// past MAX_PATH are created even with LongPathsEnabled off.
const boost::filesystem::path dest_path(dest_file);
const boost::filesystem::path io_dest = extended_path(dest_path);
if (stat.m_is_directory) {
const boost::filesystem::path dest_path(dest_file);
if (!boost::filesystem::exists(dest_path))
boost::filesystem::create_directories(dest_path);
if (!boost::filesystem::exists(io_dest))
boost::filesystem::create_directories(io_dest);
continue;
}
if (stat.m_uncomp_size == 0) {
@@ -167,10 +213,9 @@ bool extract_archive_confined(const std::string &zip_path_utf8, const std::strin
continue;
}
// Replace a symlink at the destination rather than writing through it.
const boost::filesystem::path dest_path(dest_file);
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
boost::filesystem::remove(dest_path);
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(io_dest)))
boost::filesystem::remove(io_dest);
if (!extract_zip_entry_to_file(&archive, stat.m_file_index, dest_path)) {
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
close_zip_reader(&archive);
return false;
+5
View File
@@ -1,6 +1,7 @@
#ifndef MINIZ_EXTENSION_HPP
#define MINIZ_EXTENSION_HPP
#include <boost/filesystem/path.hpp>
#include <string>
#include <miniz.h>
@@ -13,6 +14,10 @@ bool close_zip_writer(mz_zip_archive *zip);
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
// Extracts one entry into dest_path, binary-safe and overwriting. On Windows the destination is
// opened through an extended-length path so it works past MAX_PATH even with LongPathsEnabled off;
// a partial file is removed if the write fails. False for a directory/unsupported entry or on error.
bool extract_zip_entry_to_file(mz_zip_archive *zip, mz_uint file_index, const boost::filesystem::path &dest_path);
class MZ_Archive {
public:
+40
View File
@@ -2254,5 +2254,45 @@ bool check_layer_id_pattern(const std::string& pattern, int layer_id){
return mod >= 0 && mod < count;
}
boost::filesystem::path extended_path(const boost::filesystem::path& path) {
#ifdef WIN32
namespace fs = boost::filesystem;
// Already extended, or an explicit device path (which is not subject to MAX_PATH and must not
// be absolutized or normalized): leave it alone.
{
fs::path prefix_candidate = path;
const std::wstring native = prefix_candidate.make_preferred().wstring();
if (native.rfind(L"\\\\?\\", 0) == 0 || native.rfind(L"\\\\.\\", 0) == 0)
return fs::path(native);
}
fs::path absolute = path;
if (!absolute.is_absolute()) {
boost::system::error_code ec;
absolute = fs::absolute(absolute, ec);
if (ec)
return path; // can't form a valid prefixed path from a relative one
}
// A trailing separator is redundant to the Win32 APIs, but lexically_normal() would rewrite it
// as a "." element, which the "\\?\" prefix (normalisation off) takes literally. Drop it first.
while (absolute.has_relative_path() && absolute.filename() == ".")
absolute = absolute.parent_path();
// The "\\?\" prefix turns off Win32 normalisation, so do it here first.
absolute = absolute.lexically_normal();
// Extended paths only recognize backslash separators.
const std::wstring native = absolute.make_preferred().wstring();
if (native.empty())
return path;
if (native.rfind(L"\\\\", 0) == 0)
return fs::path(L"\\\\?\\UNC\\" + native.substr(2)); // \\server\share\... -> \\?\UNC\server\share\...
return fs::path(L"\\\\?\\" + native); // C:\dir\... -> \\?\C:\dir\...
#else
return path;
#endif
}
}; // namespace Slic3r
+6
View File
@@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoUtils.hpp
GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp
GUI/SequentialPrintClearance.hpp
GUI/GLModel.cpp
GUI/GLModel.hpp
GUI/GLSelectionRectangle.cpp
@@ -689,6 +690,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/host/PluginResourceTextures.cpp
plugin/host/PluginResourceTextures.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -712,6 +715,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+15 -91
View File
@@ -65,6 +65,7 @@
#include <string_view>
#include "slic3r/GUI/Selection.hpp"
#include "GLCanvas3D.hpp"
#include "SequentialPrintClearance.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif
@@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance()
//BBS: add the height logic
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
Polygons polygons;
std::vector<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count);
height_polygons.reserve(instances_count);
std::vector<struct height_info> convex_and_bounding_boxes;
struct height_info
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
};
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
convex_and_bounding_boxes.reserve(instances_count);
std::map<ObjectID, int> print_order;
for (const PrintObject* print_object : fff_print()->objects())
for (const PrintInstance& instance : print_object->instances())
print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order);
for (size_t i = 0; i < instance_transforms.size(); ++i) {
const size_t first_instance = convex_and_bounding_boxes.size();
const auto& instances = instance_transforms[i];
double rotation_z0 = instances.front()->get_rotation().z();
int index = 0;
@@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance()
Polygon convex_hull(std::move(inst_pts));
BoundingBox bouding_box = convex_hull.bounding_box();
BoundingBox plate_bb = plate->get_bounding_box_crd();
const ObjectID instance_id = m_model->objects[i]->instances[index]->id();
double instance_height = m_model->objects[i]->get_instance_max_z(index++);
//skip the object for not current plate
if (!plate_bb.overlap(bouding_box))
continue;
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull});
convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id});
polygons.emplace_back(std::move(convex_hull));
}
sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order);
}
//sort the print instance
std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(),
[](auto &l, auto &r) {
auto ly1 = l.bounding_box.min.y();
auto ly2 = l.bounding_box.max.y();
auto ry1 = r.bounding_box.min.y();
auto ry2 = r.bounding_box.max.y();
auto inter_min = std::max(ly1, ry1);
auto inter_max = std::min(ly2, ry2);
auto lx = l.bounding_box.min.x();
auto rx = r.bounding_box.min.x();
if (inter_max - inter_min > 0)
return (lx < rx) || ((lx == rx)&&(ly1 < ry1));
else
return (ly1 < ry1);
});
/*bool has_interlaced_objects = false;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
has_interlaced_objects = true;
break;
}
}
if (has_interlaced_objects)
break;
}*/
int bounding_box_count = convex_and_bounding_boxes.size();
double printable_height = fff_print()->config().printable_height;
double hc1 = fff_print()->config().extruder_clearance_height_to_lid;
double hc2 = fff_print()->config().extruder_clearance_height_to_rod;
for (int k = 0; k < bounding_box_count; k++)
{
Polygon& convex = convex_and_bounding_boxes[k].hull_polygon;
BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box;
auto iy1 = bbox.min.y();
auto iy2 = bbox.max.y();
double height = (k == (bounding_box_count - 1))?printable_height:hc1;
/*if (has_interlaced_objects) {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) {
height_polygons.emplace_back(std::make_pair(convex, hc2));
}
}
else {
if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) {
height_polygons.emplace_back(std::make_pair(convex, hc1));
}
}*/
for (int i = k+1; i < bounding_box_count; i++)
{
Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon;
BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box;
auto py1 = next_bbox.min.y();
auto py2 = next_bbox.max.y();
auto inter_min = std::max(iy1, py1); // min y of intersection
auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists
if (inter_max - inter_min > 0) {
height = hc2;
break;
}
}
if (height < convex_and_bounding_boxes[k].instance_height)
height_polygons.emplace_back(std::make_pair(convex, height));
}
const auto& config = fff_print()->config();
const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height,
config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod);
// sends instances 2d hulls to be rendered
set_sequential_print_clearance_visible(true);
+108 -15
View File
@@ -252,9 +252,11 @@
#include "slic3r/GUI/ParamsDialog.hpp"
#include "slic3r/GUI/ParamsPanel.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
#include <wx/defs.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#include <cwchar>
@@ -333,6 +335,23 @@ static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent*
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
}
// Full {web} page URL for a same-site OrcaCloud path, or empty when it cannot be built. An already
// open cloud pane is navigated straight to it, without re-running the session handoff.
static wxString cloud_page_url(NetworkAgent* agent, const std::string& path)
{
if (path.empty())
return {};
auto orca = get_orca_cloud_agent(agent);
if (!orca)
return {};
std::string base = orca->get_cloud_base_url();
while (!base.empty() && base.back() == '/')
base.pop_back();
if (base.empty())
return {};
return from_u8(base + path);
}
void start_ping_test()
{
return;
@@ -3063,6 +3082,37 @@ void GUI_App::init_plugin_gui_wiring()
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_register(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_plugin_deregister(plugin_key);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_load_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_register(capability);
Slic3r::GUI::bump_texture_library_generation();
});
plugin_mgr.subscribe_on_capability_unload_callback([](const PluginCapabilityId& capability) {
if (capability.type != PluginCapabilityType::ResourceTexture)
return;
if (wxTheApp == nullptr || wxGetApp().is_closing())
return;
PluginResourceTextures::instance().on_cap_deregister(capability);
Slic3r::GUI::bump_texture_library_generation();
});
// Packs already loaded before this wiring, or in tests where no callback fires.
PluginResourceTextures::instance().initial_sweep();
Slic3r::GUI::bump_texture_library_generation();
}
bool GUI_App::on_init_inner()
@@ -5434,12 +5484,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
// Deferred: set_cloud_mode() may create a second native webview, which must
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
this->open_orca_cloud();
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
@@ -5588,6 +5633,22 @@ std::string GUI_App::handle_web_request(std::string cmd)
return "";
}
void GUI_App::open_orca_cloud(const std::string& next)
{
// Remembered so a login while the pane is open re-seeds the same page rather than the default.
m_cloud_open_path = next;
// Deferred: set_cloud_mode() may create the second native webview, so this must not run inside
// the caller's frame (a webview script-message callback or an ImGui frame).
CallAfter([this]() {
if (mainframe != nullptr)
mainframe->select_tab(TAB_ID_HOME);
// The Home page is built lazily, so the tab switch may not have created it yet: ensure() builds
// it if needed, matching what showing the tab does, rather than dropping the open request.
if (WebViewPanel* home = WebViewPanel::ensure())
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path), cloud_page_url(m_agent, m_cloud_open_path));
});
}
void GUI_App::handle_script_message(std::string msg, const std::string& provider)
{
try {
@@ -5865,7 +5926,7 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/
}
wxString GUI_App::cloud_webview_handoff()
wxString GUI_App::cloud_webview_handoff(const std::string& next)
{
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
@@ -5873,7 +5934,7 @@ wxString GUI_App::cloud_webview_handoff()
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return {};
return from_u8(orca->build_web_session_start_url());
return from_u8(orca->build_web_session_start_url(next));
}
void GUI_App::request_cloud_web_session(const std::string& state)
@@ -6016,7 +6077,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff());
home->set_cloud_mode(true, cloud_webview_handoff(m_cloud_open_path));
}
@@ -6863,7 +6924,7 @@ void GUI_App::push_notification(const MachineObject* obj, wxString msg, wxStrin
}
}
void GUI_App::reload_settings()
bool GUI_App::reload_settings()
{
if (preset_bundle && m_agent) {
// Load user's personal presets
@@ -6871,7 +6932,7 @@ void GUI_App::reload_settings()
int result = m_agent->get_user_presets(&user_presets);
if (result != 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": get_user_presets failed with code " << result << ", skipping sync";
return;
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
@@ -6946,13 +7007,30 @@ void GUI_App::reload_settings()
refresh_synced_ui();
else
CallAfter(refresh_synced_ui);
return true;
}
return false;
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
// One check at a time: the startup check can come due while the manual one is open, and on macOS the menu bar
// stays usable while a modal dialog is up.
if (m_legacy_filament_check_running)
return;
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
// The local copies may be older than the cloud ones another machine changed: migrating them would push changes
// made against stale copies and conflict with the cloud. The startup check waits for the presets to be pulled.
if (!by_user && m_user_preset_pull_pending) {
m_legacy_filament_check_due = true;
return;
}
// This check also answers a startup check that is still waiting.
m_legacy_filament_check_due = false;
m_legacy_filament_check_running = true;
ScopeGuard running_guard([this] { m_legacy_filament_check_running = false; });
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
@@ -6997,6 +7075,9 @@ void GUI_App::check_legacy_user_filament_presets(bool by_user)
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
// Ask the sync loop to push on its next pass instead of its next 60 s round, which shortens the time the migrated
// presets stay unpushed.
m_sync_user_presets_now.store(true, std::memory_order_release);
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
@@ -7763,6 +7844,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
BOOST_LOG_TRIVIAL(info) << "start_sync_service...";
// BBS
m_user_sync_token.reset(new int(0));
m_user_preset_pull_pending = true;
if (with_progress_dlg) {
// Mark a manual progress dialog as active so restart_sync_user_preset() ignores
// repeat triggers while it is on screen (prevents stacking modal dialogs).
@@ -7836,8 +7918,16 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
finishFn(ret == 0);
if (ret == 0 && m_agent && !t.expired())
reload_settings();
// Queued after the refresh reload_settings() queues, so it runs once the pulled presets are loaded.
if (ret == 0 && m_agent && !t.expired() && reload_settings())
CallAfter([this, t] {
// A stopped or restarted sync: the next sync, if one starts, pulls the presets again.
if (t.expired() || is_closing())
return;
m_user_preset_pull_pending = false;
if (m_legacy_filament_check_due)
check_legacy_user_filament_presets(false);
});
// For orca specific syncing
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent());
@@ -7852,8 +7942,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Sync once immediately, then every 60 seconds.
while (!t.expired()) {
++tick_tock;
// Sync once immediately, then every 60s, or right away when a force-push asked for it.
if (tick_tock % 120 == 0 || m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel)) {
// Sync once immediately, then every 60s, or right away when a force-push or a preset migration asked
// for it. The request is consumed on every tick, so one made before a scheduled pass is served by it
// instead of running a second pass straight after.
const bool push_now = m_sync_user_presets_now.exchange(false, std::memory_order_acq_rel);
if (tick_tock % 120 == 0 || push_now) {
tick_tock = 0;
if (m_agent) {
if (!m_agent->is_user_login()) {
+13 -3
View File
@@ -355,6 +355,9 @@ private:
std::atomic<bool> m_restart_sync_pending {false};
std::atomic<bool> m_sync_user_preset_dlg_active {false}; // a manual "Sync Presets" progress dialog is on screen (see restart_sync_user_preset)
std::atomic<bool> m_sync_user_presets_now {false}; // request the sync loop to push user presets on its next tick
bool m_user_preset_pull_pending {false}; // set when a sync thread starts, cleared once its pull is applied; a failed pull or a stopped sync leaves it set
bool m_legacy_filament_check_due {false}; // the startup check waits for that pull
bool m_legacy_filament_check_running {false}; // a check is in progress, its dialog may be open
std::atomic<bool> m_migration_retry_pending {false};
bool m_is_dark_mode{ false };
bool m_adding_script_handler { false };
@@ -365,6 +368,8 @@ private:
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts").
std::string m_cloud_handoff_state;
// The OrcaCloud path the open requests target, remembered so a login re-seed keeps the same page.
std::string m_cloud_open_path;
uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
@@ -466,6 +471,9 @@ public:
#if wxUSE_WEBVIEW_EDGE
void init_webview_runtime();
#endif
// Switches to the Home tab and opens the OrcaCloud pane beside the homepage on `next`, a
// same-site OrcaCloud path (empty = the page's default). Work is deferred internally.
void open_orca_cloud(const std::string& next = std::string());
static unsigned get_colour_approx_luma(const wxColour& colour);
static bool dark_mode();
const wxColour get_label_default_clr_system();
@@ -572,7 +580,7 @@ public:
// called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview.
wxString cloud_webview_handoff();
wxString cloud_webview_handoff(const std::string& next = std::string());
void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
@@ -589,7 +597,8 @@ public:
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; by_user also reports when there is nothing to migrate.
// reminded; while the user presets sync with the cloud, it waits until they have been pulled. by_user also
// reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
@@ -600,7 +609,8 @@ public:
std::string format_IP(const std::string& ip);
void show_dialog(wxString msg);
void push_notification(const MachineObject* obj, wxString msg, wxString title = wxEmptyString, UserNotificationStyle style = UserNotificationStyle::UNS_NORMAL);
void reload_settings();
// Returns false when the cloud presets could not be fetched, so nothing was reloaded.
bool reload_settings();
void remove_user_presets();
bool maybe_migrate_user_presets_on_login();
@@ -40,6 +40,7 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "GLGizmoUtils.hpp"
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <functional>
@@ -72,6 +73,7 @@
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
@@ -97,6 +99,48 @@ namespace {
// bigger jump than the whole useful region.
constexpr float ImGuiLogSlider = float(ImGuiSliderFlags_Logarithmic);
// The scrollbar of the panel's scrolling areas (the panel body and the texture library). ImGui's stock
// scrollbar is a wide, square-cornered slab in a tinted track - against the flat panel it reads as a raw
// widget bolted onto the edge - so it is slimmed to a rounded thumb over an invisible track. The child
// background goes transparent with it, so the area reads as part of the panel.
void push_slim_scrollbar_style(float width, bool dark)
{
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, width);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * width);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
}
void pop_slim_scrollbar_style()
{
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
}
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
std::string ellipsize(std::string s, float max_w, float font_size = 0.f)
{
const auto text_width = [&](const std::string &text) {
return font_size > 0.f
? ImGui::GetFont()->CalcTextSizeA(font_size, std::numeric_limits<float>::max(), 0.f, text.c_str()).x
: ImGui::CalcTextSize(text.c_str()).x;
};
if (text_width(s) <= max_w)
return s;
while (!s.empty() && text_width(s + "...") > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
}
// Uploads decoded 8-bit grayscale height data as an RGBA GPU texture for display in the panel.
// Shared by the per-layer thumbnail and the picker's library entries, which differ only in where
// their pixels came from.
@@ -130,6 +174,10 @@ constexpr int HEIGHT_TEX_MAX_PX = 2048;
// The full-size image is only ever adjusted once, on commit.
constexpr int ADJUST_PREVIEW_MAX_PX = 384;
// Library textures the texture picker decodes per frame. Each decode runs on the GUI thread, so the
// rest of the visible tiles fill in over the following frames instead of in one stall.
constexpr int PICKER_LOADS_PER_FRAME = 2;
// How many rings of vertex-adjacent triangles either side of the paint's edge join the border refine
// band (see collect_paint_region()). Two is enough to grade the size change without the band's own
// cost growing to matter: it is a ring around a perimeter, not an area.
@@ -2919,8 +2967,9 @@ void GLGizmoTextureDisplacement::run_uv_command(int cmd, float value)
}
else if (cmd == int(Command::PickTexture)) {
// The same texture library the layer card opens; the panel renders it this frame.
m_picker_slot = m_active_layer_slot;
m_picker_open_request = true;
m_picker_slot = m_active_layer_slot;
m_picker_open_request = true;
m_picker_reveal_selected = true;
} else if (cmd == int(Command::Unwrap)) {
// The solve runs only on this - painting, the seam angle and seam marking never trigger it.
m_uv_unwrap_pending = true;
@@ -4073,7 +4122,7 @@ const GLGizmoTextureDisplacement::LibraryTexture *GLGizmoTextureDisplacement::ge
if (entry.image_data) {
TextureDisplacementLayer probe;
probe.image_data = entry.image_data;
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe));
entry.thumbnail = upload_height_thumbnail(decode_height_texture(probe, /* keep_decoded */ false));
if (!entry.thumbnail)
entry.image_data.reset(); // decoded to nothing usable - treat it as a failed load
} else {
@@ -4102,7 +4151,7 @@ void GLGizmoTextureDisplacement::set_layer_texture(TextureDisplacementLayer &lay
// the PNG the first time the layer is previewed or baked.
layer.image_data = tex->image_data;
// The user's own textures are listed most recently used first.
if (entry.is_user)
if (entry.source == TextureSource::User)
touch_user_texture(entry.path);
rebuild_preview();
@@ -4129,6 +4178,40 @@ void GLGizmoTextureDisplacement::import_custom_texture(TextureDisplacementLayer
set_layer_texture(layer, *entry);
}
void GLGizmoTextureDisplacement::ease_wheel_scroll(float &target, float &applied)
{
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset, a SetScroll*() call - has to re-seed the target, or the
// easing below would immediately drag the view back to where it last animated to.
if (applied < 0.f || std::abs(scroll_now - applied) > 0.5f)
target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
target = std::floor(std::clamp(target, 0.f, scroll_max));
const float delta = target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
applied = scroll_now;
}
}
void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max)
{
ModelVolume *mv = texture_volume();
@@ -4145,17 +4228,55 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
ImGui::OpenPopup(popup_id);
}
const float tile = std::round(m_imgui->scaled(3.2f));
const float spacing = std::round(m_imgui->scaled(0.4f));
const float grid_w = 4.f * tile + 3.f * spacing;
const float gap = m_imgui->scaled(0.5f);
const float tile = std::round(m_imgui->scaled(3.2f));
const float spacing = std::round(m_imgui->scaled(0.4f));
const float grid_w = 4.f * tile + 3.f * spacing;
const float gap = m_imgui->scaled(0.5f);
const float scrollbar_w = m_imgui->scaled(0.5f);
const float ring_w = std::max(2.f, std::round(m_imgui->scaled(0.1f))); // the current texture's ring
const float frame_h = ImGui::GetFrameHeight();
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
const ImGuiStyle &style = ImGui::GetStyle();
// A fixed size rather than a fit-to-content one. The width is the four-column grid, a gutter and the
// slim scrollbar beside it, plus the window padding, so the columns always fit and no horizontal
// scrollbar is needed; it derives from scaled values, so it tracks the DPI like the rest.
const float fixed_w = grid_w + spacing + scrollbar_w + 2.f * style.WindowPadding.x;
// BeginPopup() ORs in ImGuiWindowFlags_AlwaysAutoResize (deps_src/imgui/imgui.cpp:8371) and no flag
// turns it back off. A SetNextWindowSize() overrides it for each axis it sets (imgui.cpp:5983-5996),
// so an explicit size is the only way to fix the popup's size; the grid child below then scrolls.
//
// Top-aligned with the panel, which keeps it clear of the gizmo toolbar above a docked panel, and as
// tall as the panel or 70% of the window, whichever is taller, while keeping a gap inside the window.
// The height follows the window and the panel, never the content, so it does not jump as packs load or
// sections collapse.
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const float vp_top = viewport->Pos.y + gap;
const float vp_bottom = viewport->Pos.y + viewport->Size.y - gap;
const float height = std::max(std::min(std::max(0.70f * viewport->Size.y, panel_max.y - panel_min.y), vp_bottom - vp_top),
2.f * frame_h);
const float popup_y = std::max(vp_top, std::min(panel_min.y, vp_bottom - height));
// Beside the panel rather than over it, so the layer being retextured stays in view: to its left when
// there is room (the docked panel sits against the right edge of the canvas), otherwise to its right.
const float popup_w = grid_w + 2.f * ImGui::GetStyle().WindowPadding.x;
const bool left = panel_min.x - gap - popup_w >= 0.f;
ImGui::SetNextWindowPos(ImVec2(left ? panel_min.x - gap : panel_max.x + gap, panel_min.y + m_imgui->scaled(4.f)),
ImGuiCond_Appearing, ImVec2(left ? 1.f : 0.f, 0.f));
if (!ImGui::BeginPopup(popup_id))
const float width = viewport->Size.x > 0.f ? std::min(fixed_w, viewport->Size.x) : fixed_w;
const bool left = panel_min.x - gap - width >= 0.f;
const float popup_x = left ? panel_min.x - gap - width : panel_max.x + gap;
// Cond_Always, not Appearing: a viewport resize while the popup is open must move and resize it too.
ImGui::SetNextWindowPos(ImVec2(popup_x, popup_y), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
// Drawn as the panel's own sheet: its opaque background and rounding, no border. A popup is filled with
// PopupBg, which the panel's toolbar style leaves at ImGui's translucent near-black - a dark window
// beside a light panel, with whatever it covers showing through. Only Begin reads these, so they are
// popped at once and the tooltips shown from inside keep their own look.
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, style.WindowRounding);
ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, 0.f);
// The window itself never scrolls; the grid child below does.
const bool open = ImGui::BeginPopup(popup_id, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!open)
return;
if (layer == nullptr) {
ImGui::CloseCurrentPopup();
@@ -4171,16 +4292,51 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
m_imgui->push_bold_font();
m_imgui->text(_L("Choose a texture"));
m_imgui->pop_bold_font();
// The header stays put while the grid scrolls under it: the title says what the popup is for, and the
// close button is there for anyone who does not know that a click outside closes it.
bool close_clicked = false;
{
ImDrawList *hdl = ImGui::GetWindowDrawList();
const ImVec2 row = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + 0.5f * (frame_h - ImGui::GetTextLineHeight())));
m_imgui->bold_text(_u8L("Choose a texture"));
const ImVec2 btn(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x - frame_h, row.y);
ImGui::SetCursorScreenPos(btn);
close_clicked = ImGui::InvisibleButton("##close", ImVec2(frame_h, frame_h));
const bool hot = ImGui::IsItemHovered();
const ImVec2 c(btn.x + 0.5f * frame_h, btn.y + 0.5f * frame_h);
const float k = std::round(0.18f * frame_h);
const float line_w = std::max(1.f, std::round(m_imgui->scaled(0.08f)));
const ImU32 x_col = ImGui::GetColorU32(ImGuiCol_Text, hot ? 1.f : 0.6f);
if (hot)
hdl->AddCircleFilled(c, 0.5f * frame_h, ImGui::GetColorU32(ImGuiCol_Text, 0.08f));
hdl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), x_col, line_w);
hdl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), x_col, line_w);
ImGui::SetCursorScreenPos(ImVec2(row.x, row.y + frame_h + style.ItemSpacing.y));
ImGui::Separator();
}
const std::vector<TextureLibraryEntry> &library = texture_library();
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 selected_col = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
std::optional<TextureLibraryEntry> chosen;
std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false;
// A reinstalled pack can drop new bytes at a path already cached on this gizmo, and get_library_texture()
// caches a failure as well as a success, so a moved generation makes every cached entry suspect.
if (const uint64_t generation = texture_library_generation(); generation != m_library_generation) {
m_library_textures.clear();
m_library_generation = generation;
}
// The grid scrolls as the panel body does: the same slim scrollbar, and the wheel eased by
// ease_wheel_scroll() instead of ImGui's five-line jumps (hence NoScrollWithMouse).
push_slim_scrollbar_style(scrollbar_w, wxGetApp().dark_mode());
ImGui::BeginChild("##texture_grid", ImVec2(0.f, 0.f), false, ImGuiWindowFlags_NoScrollWithMouse);
ease_wheel_scroll(m_picker_scroll_target, m_picker_scroll_applied);
ImDrawList *dl = ImGui::GetWindowDrawList(); // the child's, so tiles clip to the grid
std::optional<TextureLibraryEntry> chosen;
std::optional<TextureLibraryEntry> to_remove;
bool import_clicked = false;
bool download_clicked = false;
int loads_left = PICKER_LOADS_PER_FRAME;
bool loads_deferred = false;
// Tiles are placed on the four-column grid explicitly rather than flowed with SameLine(): a user tile
// overlays a remove button on its corner, and that button would otherwise be "the previous item" the
@@ -4196,16 +4352,34 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
ImGui::Dummy(ImVec2(grid_w, float(rows) * tile + float(rows - 1) * spacing));
}
};
// One group's tiles: shipped textures alphabetically, or the user's own most recently used first (the
// order texture_library() keeps), those with a remove button on hover. Returns how many it drew.
const auto grid = [&](const ImVec2 &origin, bool user) {
// One section's tiles, in the order the section holds them (shipped alphabetically, pack textures by
// name, the user's own most recently used first - the order texture_library() produced); a user tile
// carries a remove button on hover. Returns how many tiles it laid out, drawn or not.
const auto grid = [&](const ImVec2 &origin, const std::vector<const TextureLibraryEntry *> &entries, bool removable) {
int n = 0;
for (const TextureLibraryEntry &entry : library) {
if (entry.is_user != user)
for (const TextureLibraryEntry *entry : entries) {
const ImVec2 rect_min = tile_pos(origin, n++);
const ImVec2 rect_max = ImVec2(rect_min.x + tile, rect_min.y + tile);
const bool current = entry->path == layer->path;
// The selected tile is centred even when it is off screen, so it is drawn and loaded next frame.
if (current && m_picker_reveal_selected) {
ImGui::SetScrollFromPosY(0.5f * (rect_min.y + rect_max.y) - ImGui::GetWindowPos().y, 0.5f);
m_parent.request_extra_frame();
}
if (!ImGui::IsRectVisible(rect_min, rect_max))
continue;
ImGui::SetCursorScreenPos(tile_pos(origin, n++));
ImGui::PushID(entry.path.c_str());
const LibraryTexture *tex = get_library_texture(entry.path);
// Thumbnails decode on this thread, so only a few per frame; the rest fill in on later frames.
const bool cached = m_library_textures.count(entry->path) != 0;
if (!cached && loads_left == 0) {
loads_deferred = true;
dl->AddRectFilled(rect_min, rect_max, ImGui::GetColorU32(ImGuiCol_FrameBg), style.FrameRounding);
continue;
}
if (!cached)
--loads_left;
ImGui::SetCursorScreenPos(rect_min);
ImGui::PushID(entry->path.c_str());
const LibraryTexture *tex = get_library_texture(entry->path);
GLTexture *thumb = tex != nullptr ? tex->thumbnail.get() : nullptr;
bool clicked = false;
if (thumb != nullptr) {
@@ -4223,15 +4397,23 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
} else {
clicked = ImGui::Button("?", ImVec2(tile, tile));
}
if (user)
if (removable)
ImGui::SetItemAllowOverlap(); // the remove button drawn over its corner takes its own clicks
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
if (entry.path == layer->path)
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f);
if (ImGui::IsItemHovered())
panel_tooltip(entry.name);
const ImVec2 a = ImGui::GetItemRectMin(), b = ImGui::GetItemRectMax();
const bool hovered = ImGui::IsItemHovered();
// The layer's current texture carries an accent ring, and the tile under the pointer a thinner,
// fainter one. Both are drawn inside the tile, where the grid's clip rect never cuts them.
if (current || hovered) {
const float w = current ? ring_w : std::round(0.5f * ring_w);
dl->AddRect(ImVec2(a.x + 0.5f * w, a.y + 0.5f * w), ImVec2(b.x - 0.5f * w, b.y - 0.5f * w),
ImGui::GetColorU32(current ? orca : ImVec4(orca.x, orca.y, orca.z, 0.6f)), 0.f, 0, w);
}
if (hovered)
panel_tooltip(entry->name);
if (user && ImGui::IsMouseHoveringRect(a, b)) {
// Clipped by the grid's clip rect, so a tile scrolled out of the grid or under its scrollbar
// does not offer its remove button. IsMouseHoveringRect() clips by default.
if (removable && ImGui::IsMouseHoveringRect(a, b)) {
const float r = std::round(tile * 0.17f);
const ImVec2 c(b.x - r - 2.f, a.y + r + 2.f);
ImGui::SetCursorScreenPos(ImVec2(c.x - r, c.y - r));
@@ -4244,27 +4426,162 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
if (hot)
panel_tooltip(_u8L("Remove from My textures"));
if (remove_clicked)
to_remove = entry;
to_remove = *entry;
}
ImGui::PopID();
if (clicked)
chosen = entry;
chosen = *entry;
}
return n;
};
ImGui::TextDisabled("%s", _u8L("Built-in").c_str());
// Every section collapses; the state is remembered for the app run. Pack keys use pack_name, a
// capability name unique per type across loaded packages and stable across reinstalls. This function-local
// static survives popup closes, never touches app_config, and resets on restart.
static std::map<std::string, bool> s_section_collapsed; // true = collapsed; absent = expanded
const auto split_pack_title = [](const std::string &title) -> std::pair<std::string, std::string> {
size_t split = std::string::npos;
for (size_t i = 0; i + 3 < title.size(); ++i)
if (title[i] == ' ' && (title[i + 1] == 'b' || title[i + 1] == 'B') &&
(title[i + 2] == 'y' || title[i + 2] == 'Y') && title[i + 3] == ' ')
split = i;
if (split == std::string::npos)
return { title, {} };
std::string name = title.substr(0, split);
while (!name.empty() && std::isspace(static_cast<unsigned char>(name.back())))
name.pop_back();
std::string attribution = title.substr(split);
size_t first = 0;
while (first < attribution.size() && std::isspace(static_cast<unsigned char>(attribution[first])))
++first;
attribution.erase(0, first);
if (name.empty() || attribution.empty())
return { title, {} };
return { name, attribution };
};
// A section heading, one row as wide as the grid: a click anywhere on it collapses or expands the
// section. Child headings indent their contents while keeping their counts right-aligned. Attribution
// lines use a smaller disabled style; the grid follows closer than the next heading does.
// Returns true when the section is open, so the caller knows whether to draw its contents.
const auto section_header = [&](const std::string &key, const std::string &title, size_t count, float indent,
const std::string &attribution, const std::string &full_title) {
bool &collapsed = s_section_collapsed[key]; // default-inserts false -> expanded
const ImVec2 row = ImGui::GetCursorScreenPos();
const bool two_lines = !attribution.empty();
const float line_h = ImGui::GetTextLineHeight();
const float small_font_size = ImGui::GetFontSize() * 0.8f;
const float small_line_h = line_h * 0.8f;
const float row_h = two_lines ? std::max(frame_h, line_h + small_line_h + spacing) : frame_h;
const ImVec2 row_max(row.x + grid_w, row.y + row_h);
if (ImGui::InvisibleButton(key.c_str(), ImVec2(grid_w, row_h)))
collapsed = !collapsed;
const bool hovered = ImGui::IsItemHovered();
if (hovered)
dl->AddRectFilled(row, row_max, ImGui::GetColorU32(ImGuiCol_Text, 0.05f), style.FrameRounding);
const float inset = std::round(0.25f * frame_h);
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
const float cx = row.x + inset + indent + 0.5f * arrow;
const float cy = row.y + 0.5f * row_h;
const float hs = 0.3f * arrow;
const ImU32 accent = ImGui::GetColorU32(orca);
if (collapsed)
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx + 0.5f * hs, cy), ImVec2(cx - 0.5f * hs, cy + hs), accent);
else
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs), ImVec2(cx, cy + 0.5f * hs), accent);
const std::string n = std::to_string(count);
const float n_w = ImGui::CalcTextSize(n.c_str()).x;
const float tx = row.x + inset + indent + arrow + spacing;
const float max_text_w = std::max(0.f, row_max.x - inset - n_w - spacing - tx);
const std::string shown = ellipsize(title, max_text_w);
const std::string shown_attribution = two_lines ? ellipsize(attribution, max_text_w, small_font_size) : std::string{};
const float title_y = two_lines ? row.y + 0.5f * (row_h - line_h - small_line_h)
: row.y + 0.5f * (frame_h - line_h);
const float count_y = row.y + 0.5f * (row_h - line_h);
dl->AddText(ImVec2(tx, title_y), ImGui::GetColorU32(ImGuiCol_Text), shown.c_str());
if (two_lines)
dl->AddText(ImGui::GetFont(), small_font_size, ImVec2(tx, title_y + line_h),
ImGui::GetColorU32(ImGuiCol_TextDisabled), shown_attribution.c_str());
dl->AddText(ImVec2(row_max.x - inset - n_w, count_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), n.c_str());
if (hovered && (shown != title || shown_attribution != attribution))
panel_tooltip(full_title); // a long plugin-supplied name, cut by the fixed width
ImGui::SetCursorScreenPos(ImVec2(row.x, row_max.y + spacing));
return !collapsed;
};
// Built-in is first, plugin packs are grouped under Texture libraries, and My
// textures stays last so its import tile and help line describe that group. Pack names identify both
// their child section and state.
struct Section
{
std::string key;
std::string title;
std::vector<const TextureLibraryEntry *> entries;
};
std::vector<Section> sections;
std::map<std::string, size_t> pack_section; // pack_name -> index in `sections`
sections.push_back({ "builtin:", _u8L("Built-in"), {} });
for (const TextureLibraryEntry &entry : library) {
if (entry.source == TextureSource::Shipped) {
sections.front().entries.push_back(&entry); // the section exists even when the library is empty
continue;
}
if (entry.source == TextureSource::User)
continue; // added below, so My textures stays last
if (entry.source != TextureSource::Plugin)
continue;
const auto [it, inserted] = pack_section.emplace(entry.pack_name, sections.size());
if (inserted)
sections.push_back({ "pack:" + entry.pack_name, entry.pack_name, {} });
sections[it->second].entries.push_back(&entry);
}
sections.push_back({ "user:", _u8L("My textures"), {} });
for (const TextureLibraryEntry &entry : library)
if (entry.source == TextureSource::User)
sections.back().entries.push_back(&entry);
const float child_indent = std::round(m_imgui->scaled(0.8f));
const Section &built_in = sections.front();
if (section_header(built_in.key, built_in.title, built_in.entries.size(), 0.f, {}, built_in.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, false);
const int count = grid(origin, built_in.entries, false);
end_grid(origin, count);
if (count == 0)
ImGui::TextDisabled("%s", _u8L("No textures found.").c_str());
}
ImGui::TextDisabled("%s", _u8L("My textures").c_str());
{
size_t subscribed_count = 0;
for (size_t i = 1; i + 1 < sections.size(); ++i)
subscribed_count += sections[i].entries.size();
const std::string subscribed_title = _u8L("Texture libraries");
if (section_header("subscribed:", subscribed_title, subscribed_count, 0.f, {}, subscribed_title)) {
for (size_t i = 1; i + 1 < sections.size(); ++i) {
const Section &pack = sections[i];
const auto [name, attribution] = split_pack_title(pack.title);
if (!section_header(pack.key, name, pack.entries.size(), child_indent, attribution, pack.title))
continue;
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, pack.entries, false);
end_grid(origin, count);
}
ImGui::Dummy(ImVec2(0.f, spacing));
const std::string download_label = _u8L("Explore more texture libraries");
if (ImGui::Button(download_label.c_str(), ImVec2(grid_w, frame_h)))
download_clicked = true;
if (ImGui::IsItemHovered())
panel_tooltip(_u8L("Opens OrcaCloud in the Home page to browse for plugins."));
}
const Section &user = sections.back();
if (section_header(user.key, user.title, user.entries.size(), 0.f, {}, user.title)) {
const ImVec2 origin = ImGui::GetCursorScreenPos();
const int count = grid(origin, true);
const int count = grid(origin, user.entries, true);
ImGui::SetCursorScreenPos(tile_pos(origin, count));
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
import_clicked = true;
@@ -4273,17 +4590,27 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
"bakes from and saved to your personal texture folder, kept separate from the "
"textures shipped with OrcaSlicer."));
end_grid(origin, count + 1);
// Belongs to this section: it describes the import tile and the remove-on-hover tiles, both of
// which live here, so it is hidden with them rather than left describing controls off-screen.
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos();
}
if (loads_deferred)
m_parent.request_extra_frame();
// Done whether or not the current texture was drawn: one in a collapsed section stays where it is.
m_picker_reveal_selected = false;
ImGui::EndChild();
pop_slim_scrollbar_style();
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + grid_w);
ImGui::TextDisabled("%s", _u8L("PNG, JPG or BMP - converted to a height map on import. Hover one of yours to remove it.").c_str());
ImGui::PopTextWrapPos();
if (chosen || import_clicked)
if (chosen || import_clicked || close_clicked || download_clicked)
ImGui::CloseCurrentPopup();
m_tooltip_anchor_min = prev_anchor_min;
m_tooltip_anchor_max = prev_anchor_max;
ImGui::EndPopup();
// The OrcaCloud plugin hub filtered to resource textures; the pane's session handoff redirects here.
if (download_clicked)
wxGetApp().open_orca_cloud("/app/plugins/plugin-hub?types=resource-textures");
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
// and removing all change the library the loop was iterating.
@@ -6586,19 +6913,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(text);
m_imgui->pop_bold_font();
};
// Cuts a UTF-8 string down to `max_w` pixels, ending it in "..." when it had to be cut.
const auto ellipsize = [](std::string s, float max_w) {
if (ImGui::CalcTextSize(s.c_str()).x <= max_w)
return s;
while (!s.empty() && ImGui::CalcTextSize((s + "...").c_str()).x > max_w) {
unsigned char c;
do { // a whole code point at a time
c = static_cast<unsigned char>(s.back());
s.pop_back();
} while (!s.empty() && (c & 0xC0) == 0x80);
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
@@ -6791,52 +7105,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
style.WindowPadding.y - style.ItemSpacing.y);
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
// ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this flat panel
// it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over an invisible track.
const float scrollbar_w = m_imgui->scaled(0.5f);
const ImVec4 grab = dark ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
// NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of teleporting
// five text lines per notch, which on blocks this tall lost the reader's place.
push_slim_scrollbar_style(m_imgui->scaled(0.5f), dark);
// NoScrollWithMouse: ease_wheel_scroll() eases the scroll instead of teleporting five text lines per
// notch, which on blocks this tall lost the reader's place.
ImGui::BeginChild("##td_body", ImVec2(panel_w, body_h), false, ImGuiWindowFlags_NoScrollWithMouse);
{
ImGuiIO &io = ImGui::GetIO();
const float scroll_now = ImGui::GetScrollY();
const float scroll_max = ImGui::GetScrollMaxY();
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav step, the
// content shrinking under a clamped offset - has to re-seed the target, or the easing below would
// immediately drag the view back to where it last animated to.
if (m_panel_scroll_applied < 0.f || std::abs(scroll_now - m_panel_scroll_applied) > 0.5f)
m_panel_scroll_target = scroll_now;
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
m_panel_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could never be
// reached and the "still gliding" test below would stay true forever, repainting the canvas for good.
m_panel_scroll_target = std::floor(std::clamp(m_panel_scroll_target, 0.f, scroll_max));
const float delta = m_panel_scroll_target - scroll_now;
if (std::abs(delta) >= 1.f) {
// Exponential ease, formulated against the frame time so the glide takes the same wall time
// whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be floored
// away, so land on the target outright once the remainder is that small.
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
const float next = std::floor(scroll_now + step);
ImGui::SetScrollY(next);
m_panel_scroll_applied = next;
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
} else {
m_panel_scroll_applied = scroll_now;
}
}
ease_wheel_scroll(m_panel_scroll_target, m_panel_scroll_applied);
// Everything that changes the layer list is deferred until the list is no longer being drawn:
// removing or reordering shifts mv->texture_displacement_layers under the pointers the loop holds,
@@ -7081,8 +7354,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Opens the texture library for a layer, making it the active one too (after the loop).
const auto open_picker = [&](int slot) {
m_picker_slot = slot;
m_picker_open_request = true;
m_picker_slot = slot;
m_picker_open_request = true;
m_picker_reveal_selected = true;
};
// The six-dot drag icon, drawn into `list` over [x, x + w] x [y, y + h].
const auto draw_drag_icon = [&](ImDrawList *list, float x, float y, float w, float h, ImU32 tint) {
@@ -8077,8 +8351,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// height of everything above, whether or not it is scrolled.
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
ImGui::EndChild();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
pop_slim_scrollbar_style();
// The layer-list changes deferred from the loop above.
if (slot_to_remove >= 0)
@@ -428,6 +428,10 @@ private:
void render_texture_library_popup(const ImVec2 &panel_min, const ImVec2 &panel_max);
int m_picker_slot = -1;
bool m_picker_open_request = false;
// Set when the popup opens, so the next layout scrolls the layer's current texture into view.
bool m_picker_reveal_selected = false;
float m_picker_scroll_target = 0.f; // see ease_wheel_scroll()
float m_picker_scroll_applied = -1.f;
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
@@ -1062,6 +1066,8 @@ private:
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
// Generation of the library the above were cached against; a change clears them.
uint64_t m_library_generation = 0;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
@@ -1103,12 +1109,15 @@ private:
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
// jolt rather than a scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and
// moves a *target* offset instead; the real scroll is eased toward it over the following frames.
// Smooth scrolling for a child window, called inside it: the panel body (everything between the header
// and the pinned Bake footer) and the texture library's grid. ImGui jumps a fixed number of lines per
// wheel notch, which on tall layer cards reads as a hard jolt rather than a scroll. The child sets
// ImGuiWindowFlags_NoScrollWithMouse, and the wheel moves `target` instead; the real scroll is eased
// toward it over the following frames. `applied` is what the easing wrote last frame, <0 until the
// first one.
void ease_wheel_scroll(float &target, float &applied);
float m_panel_scroll_target = 0.f;
float m_panel_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
float m_panel_scroll_applied = -1.f;
// Last frame's body content height and footer height. The body is a child window that has to be
// given its height before its content is laid out, so it is sized from the previous frame: as tall
// as its content, capped so the footer still fits above the bottom of the canvas.
+5 -3
View File
@@ -505,10 +505,12 @@ bool ImGuiWrapper::update_mouse_data(wxMouseEvent& evt)
io.MouseDown[0] = evt.LeftIsDown();
io.MouseDown[1] = evt.RightIsDown();
io.MouseDown[2] = evt.MiddleIsDown();
float wheel_delta = static_cast<float>(evt.GetWheelDelta());
if (wheel_delta != 0.0f) {
// Only a vertical wheel event with a direction is a wheel step. A macOS trackpad reports sideways motion
// as separate horizontal events, and its wheel events often carry a rotation of 0: wx truncates the
// sub-pixel deltas to whole ones and passes on events with no delta at all. Read as vertical steps,
// either kind scrolls against the gesture, and scrolling up bounces back and forth.
if (evt.GetWheelAxis() == wxMOUSE_WHEEL_VERTICAL && evt.GetWheelRotation() != 0)
io.MouseWheel = evt.GetWheelRotation() > 0 ? 1.f : -1.f;
}
unsigned buttons = (evt.LeftIsDown() ? 1 : 0) | (evt.RightIsDown() ? 2 : 0) | (evt.MiddleIsDown() ? 4 : 0);
m_mouse_buttons = buttons;
+3
View File
@@ -1739,6 +1739,9 @@ void MainFrame::fit_tab_labels()
for (size_t i = count - 1; i >= 1; --i) {
int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x;
int left = sizer->GetSize().GetWidth();
// The overflow button sits between the tab buttons and the side tools, so it is not free space.
if (const wxWindow* overflow = ctrl->GetOverflowButton())
left += overflow->GetSize().GetWidth();
if (right - left - FromDIP(15) > 0) return;
ctrl->SetCompact(i, true);
m_tabpanel->Refresh();
+1
View File
@@ -57,6 +57,7 @@ public:
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
void SetOverflowButton(wxWindow* button);
wxWindow* GetOverflowButton() const { return m_overflow_button; }
private:
wxFlexGridSizer* m_buttons_sizer;
+3 -1
View File
@@ -2225,8 +2225,10 @@ void PublishSettingsDialog::refresh_tab_indicators()
if (section.variant_switch != nullptr) {
const wxString& title = m_categories[section.categories[i]].title;
const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title;
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text)
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text) {
section.variant_switch->SetOptionText(static_cast<unsigned int>(i), text);
section.page->Layout();
}
}
any = any || on;
}
@@ -0,0 +1,63 @@
#pragma once
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Polygon.hpp"
#include <algorithm>
#include <cstddef>
#include <map>
#include <utility>
#include <vector>
namespace Slic3r::GUI {
struct SequentialClearanceInstance
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
ObjectID instance_id;
};
// PrintApply may group rotated copies in a different order from ModelObject::instances.
// Reorder one object's instances only when validation has assigned all of them an order.
inline void sort_sequential_clearance_instances(
std::vector<SequentialClearanceInstance>::iterator first, std::vector<SequentialClearanceInstance>::iterator last,
const std::map<ObjectID, int>& print_order)
{
if (!std::all_of(first, last, [&print_order](const SequentialClearanceInstance& instance) {
const auto order = print_order.find(instance.instance_id);
return order != print_order.end() && order->second > 0;
}))
return;
std::stable_sort(first, last, [&print_order](const SequentialClearanceInstance& lhs, const SequentialClearanceInstance& rhs) {
return print_order.at(lhs.instance_id) < print_order.at(rhs.instance_id);
});
}
// Input follows object-list order with validated print order within each object.
inline std::vector<std::pair<Polygon, float>> sequential_clearance_height_polygons(
const std::vector<SequentialClearanceInstance>& instances, double printable_height, double height_to_lid, double height_to_rod)
{
std::vector<std::pair<Polygon, float>> height_polygons;
height_polygons.reserve(instances.size());
for (size_t k = 0; k < instances.size(); ++k) {
const auto& instance = instances[k];
double height = k + 1 == instances.size() ? printable_height : height_to_lid;
for (size_t i = k + 1; i < instances.size(); ++i) {
const auto& next_bbox = instances[i].bounding_box;
if (std::min(instance.bounding_box.max.y(), next_bbox.max.y()) >
std::max(instance.bounding_box.min.y(), next_bbox.min.y())) {
height = height_to_rod;
break;
}
}
if (height < instance.instance_height)
height_polygons.emplace_back(instance.hull_polygon, float(height));
}
return height_polygons;
}
} // namespace Slic3r::GUI
+75 -25
View File
@@ -1,6 +1,7 @@
#include "TextureLibrary.hpp"
#include <algorithm>
#include <atomic>
#include <boost/filesystem/operations.hpp>
#include <cstddef>
#include <boost/filesystem/directory.hpp>
@@ -21,6 +22,7 @@
#include <boost/system/error_code.hpp>
#include <vector>
#include <wx/app.h>
#include <wx/gdicmn.h>
#include <utility>
#include <wx/image.h>
@@ -36,6 +38,9 @@
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/host/PluginResourceTextures.hpp"
namespace Slic3r::GUI {
namespace {
@@ -50,7 +55,7 @@ bool is_image_file(const boost::filesystem::path &path)
return ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp";
}
void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<TextureLibraryEntry> &out)
void scan_dir(const boost::filesystem::path &dir, TextureSource source, std::vector<TextureLibraryEntry> &out)
{
boost::system::error_code ec;
if (!boost::filesystem::is_directory(dir, ec))
@@ -60,12 +65,43 @@ void scan_dir(const boost::filesystem::path &dir, bool is_user, std::vector<Text
for (boost::filesystem::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) {
if (!boost::filesystem::is_regular_file(it->path(), ec) || !is_image_file(it->path()))
continue;
out.push_back({ it->path().stem().string(), it->path().string(), is_user });
out.push_back({ it->path().stem().string(), it->path().string(), source });
}
std::sort(out.begin() + first, out.end(),
[](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return a.name < b.name; });
}
// Appends every loaded pack's textures, ordered by the pack's display name and then by file name. Each
// entry carries the contributing pack's display name, which the picker uses to group a pack's textures
// into one section; a pack name is unique among loaded packs of this capability type, so the name both
// groups the section and keeps its entries contiguous. A pack whose directory fails validation is
// skipped, so one bad pack never hides the others.
void scan_plugin_textures(std::vector<TextureLibraryEntry> &out)
{
std::vector<TextureLibraryEntry> found;
for (const ResourceTextureEntry &pack : PluginResourceTextures::instance().entries()) {
std::string error;
const std::vector<std::string> files =
collect_resource_texture_files(pack.texture_dir, pack.plugin_root, error);
if (!error.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Texture pack '" << pack.display_name << "' was skipped: " << error;
continue;
}
for (const std::string &file : files) {
const boost::filesystem::path path(file);
found.push_back({ path.stem().string(), file, TextureSource::Plugin, pack.display_name });
}
}
std::stable_sort(found.begin(), found.end(), [](const TextureLibraryEntry &a, const TextureLibraryEntry &b) {
if (a.pack_name != b.pack_name)
return a.pack_name < b.pack_name;
return a.name < b.name;
});
for (TextureLibraryEntry &entry : found)
out.push_back(std::move(entry));
}
// True if the image carries any colour at all, i.e. some pixel's channels are not all equal. A
// photo saved as RGB but actually grey should still be stored as a grayscale PNG - it is a third of
// the bytes, and the colour feature keys off has_color(), so storing a grey image as RGB would offer
@@ -187,27 +223,20 @@ std::vector<unsigned char> read_file_bytes(const std::string &path)
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
// True if these bytes are a PNG libslic3r can decode, i.e. can be stored on a layer as-is. Mirrors
// exactly what decode_height_texture() accepts: 8-bit grayscale, or 8-bit colour (whose luminance is
// the height and whose RGB is available to colour the model). Anything else is converted on load.
// True if decode_height_texture() can read these bytes, so they can be stored on a layer as-is.
// Anything else is converted on load.
bool is_supported_height_map(const std::vector<unsigned char> &bytes)
{
if (bytes.empty())
return false;
const png::ReadBuf rbuf{ bytes.data(), bytes.size() };
if (!png::is_png(rbuf))
return false;
png::ImageGreyscale gray;
if (png::decode_png(rbuf, gray) && gray.cols > 0 && gray.rows > 0)
return true;
png::ImageColorscale color;
return png::decode_colored_png(rbuf, color) && color.cols > 0 && color.rows > 0 &&
color.bytes_per_pixel >= 3 &&
color.buf.size() == color.cols * color.rows * size_t(color.bytes_per_pixel);
return !bytes.empty() && png::is_decodable_8bit_png(png::ReadBuf{ bytes.data(), bytes.size() });
}
std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false;
// The generation g_library was last built from, so texture_library() can rescan after a pack change.
uint64_t g_library_scan_generation = 0;
// Bumped by plugin callbacks, read by the GUI frame. Atomic so a read never races a write.
std::atomic<uint64_t> g_library_generation{ 0 };
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
@@ -255,6 +284,8 @@ const size_t RECENT_MAX = 200;
std::vector<std::string> recent_texture_names()
{
std::vector<std::string> names;
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to read
return names;
if (wxGetApp().app_config == nullptr)
return names;
const std::string value = wxGetApp().app_config->get(RECENT_SECTION, RECENT_KEY);
@@ -269,6 +300,8 @@ std::vector<std::string> recent_texture_names()
void store_recent_texture_names(std::vector<std::string> names)
{
if (wxTheApp == nullptr) // headless or shutting down: there is no app config to write
return;
if (wxGetApp().app_config == nullptr)
return;
if (names.size() > RECENT_MAX)
@@ -279,8 +312,9 @@ void store_recent_texture_names(std::vector<std::string> names)
wxGetApp().app_config->set(RECENT_SECTION, RECENT_KEY, value);
}
// Reorders the user's entries (the tail of `library`) by recent use. Stable, so textures never used keep
// the name order scan_dir() gave them, after every used one.
// Reorders the user's entries by recent use. Stable, so textures never used keep the name order
// scan_dir() gave them, after every used one. Only the user's own range is touched: shipped and pack
// entries must keep the order their scans produced.
void sort_user_textures(std::vector<TextureLibraryEntry> &library)
{
const std::vector<std::string> names = recent_texture_names();
@@ -291,8 +325,11 @@ void sort_user_textures(std::vector<TextureLibraryEntry> &library)
const auto it = rank.find(boost::filesystem::path(e.path).filename().string());
return it == rank.end() ? std::numeric_limits<int>::max() : it->second;
};
const auto first_user = std::find_if(library.begin(), library.end(), [](const TextureLibraryEntry &e) { return e.is_user; });
std::stable_sort(first_user, library.end(),
const auto first_user = std::find_if(library.begin(), library.end(),
[](const TextureLibraryEntry &e) { return e.source == TextureSource::User; });
const auto user_end = std::find_if(first_user, library.end(),
[](const TextureLibraryEntry &e) { return e.source != TextureSource::User; });
std::stable_sort(first_user, user_end,
[&rank_of](const TextureLibraryEntry &a, const TextureLibraryEntry &b) { return rank_of(a) < rank_of(b); });
}
@@ -311,6 +348,16 @@ bool in_user_texture_dir(const std::string &path)
} // namespace
uint64_t texture_library_generation()
{
return g_library_generation.load();
}
void bump_texture_library_generation()
{
g_library_generation.fetch_add(1);
}
std::string user_texture_dir()
{
const boost::filesystem::path dir = boost::filesystem::path(Slic3r::data_dir()) / "textures" / "displacement";
@@ -325,17 +372,20 @@ std::string user_texture_dir()
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan)
{
if (g_library_scanned && !force_rescan)
const uint64_t generation = texture_library_generation();
if (g_library_scanned && !force_rescan && generation == g_library_scan_generation)
return g_library;
g_library.clear();
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", false, g_library);
scan_dir(boost::filesystem::path(Slic3r::resources_dir()) / "textures" / "displacement", TextureSource::Shipped, g_library);
const std::string user_dir = user_texture_dir();
if (!user_dir.empty())
scan_dir(boost::filesystem::path(user_dir), true, g_library);
scan_dir(boost::filesystem::path(user_dir), TextureSource::User, g_library);
sort_user_textures(g_library);
scan_plugin_textures(g_library);
g_library_scanned = true;
g_library_scan_generation = generation;
g_library_scanned = true;
return g_library;
}
+31 -11
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_TextureLibrary_hpp_
#define slic3r_TextureLibrary_hpp_
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
@@ -10,26 +11,45 @@
namespace Slic3r::GUI {
// One selectable height-map texture in the texture-displacement gizmo's texture picker.
// Where a library texture came from. The picker lists each source under its own heading.
enum class TextureSource { Shipped, User, Plugin };
// One selectable height-map texture in the texture-displacement gizmo's texture picker. A texture that
// came from a plugin pack also carries the provenance the picker groups its sections by.
struct TextureLibraryEntry
{
std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk
bool is_user; // imported by the user, as opposed to shipped with OrcaSlicer
std::string name; // display name (the file's stem, e.g. "Wood Grain")
std::string path; // absolute path on disk
TextureSource source; // which of the three sources it came from
std::string pack_name; // contributing pack's display name; empty for Shipped and User
};
// Every available height-map texture: the ones shipped in resources/textures/displacement first, sorted
// by name, then the user's own from <data_dir>/textures/displacement - most recently used first (see
// touch_user_texture()), then any never used, by name.
// Every available height-map texture, in the order the picker lists them: the ones shipped in
// resources/textures/displacement first, by name; then the user's own from <data_dir>/textures/displacement,
// most recently used first (see touch_user_texture()) and the rest by name; then the loaded texture
// packs, ordered by pack display name and then by file name.
//
// The two live in separate directories deliberately: an app update replaces the resources tree
// wholesale, so anything the user imported has to sit somewhere that update can never overwrite or
// delete. `is_user` is what the picker uses to show them under separate headings.
// The shipped and user textures live in separate directories deliberately: an app update replaces the
// resources tree wholesale, so anything the user imported has to sit somewhere that update can never
// overwrite or delete. Pack textures are read-only like the shipped ones. Every texture carries its
// `source`; a pack texture also carries `pack_name`, and the picker groups pack textures into one
// collapsible section per pack name. A pack's name is unique among loaded packs of this capability type -
// the loader refuses a collision with "Capability collision for type ... and name ...", see
// PluginManager::check_registry_locked (PluginManager.cpp:211-225) - so the name on its own identifies a
// section and keeps that pack's entries contiguous.
//
// Scanned once and cached. Pass force_rescan after an import, or to pick up a file the user dropped
// into either folder by hand while the app was running.
// into either folder by hand while the app was running. A rescan also happens on its own when the pack
// registry changes, which is what texture_library_generation() tracks.
const std::vector<TextureLibraryEntry> &texture_library(bool force_rescan = false);
// Monotonic counter, bumped whenever the set of loaded texture packs changes. The GUI reads it to
// decide whether texture_library() must rescan, and plugin callbacks bump it. Safe to read from any
// thread at any time.
uint64_t texture_library_generation();
void bump_texture_library_generation();
// Moves one of the user's own textures to the front of the recently-used order texture_library() lists
// them in, and stores that order in the app config so it survives a restart. No-op for anything that is
// not in user_texture_dir().
+9 -1
View File
@@ -537,7 +537,7 @@ void WebViewPanel::cloud_signed_out(const wxString& signout_url)
}
}
void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
void WebViewPanel::set_cloud_mode(bool on, const wxString& url, const wxString& page_url)
{
if (on) {
// load_cloud_url records the origin from whatever it loads, including the plain cloud
@@ -546,6 +546,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
create_cloud_browser();
m_cloud_seed_url = url;
schedule_cloud_load(url.empty() ? wxString(kCloudUrl) : url);
} else if (!page_url.empty()) {
// The pane already holds a session, so an explicit page loads straight in; the handoff is
// only needed to establish one, not to reach an ordinary page.
load_cloud_url(page_url);
} else if (!url.empty() && url != m_cloud_seed_url) {
// Re-seed only when the Start URL actually changed — a login, an account switch or a
// sign-out changes it; an ordinary re-open does not (guide §8). Reloading an unchanged
@@ -556,6 +560,10 @@ void WebViewPanel::set_cloud_mode(bool on, const wxString& url)
load_cloud_after_signout(url);
}
m_cloud_browser->Show();
// The homepage shell owns the nav highlight and the cloud-mode layout class. A native open
// (not the page's own GotoCloud) must ask it to switch; the page's SetCloudMode no-ops if it
// already did. OnDocumentLoaded re-applies this when the homepage was still loading.
RunScript("SetCloudMode(true)");
} else if (m_cloud_browser) {
// Closing keeps the pane alive with its session, so re-opening does not have to sign in
// again (guide §10). Nothing is cleared here.
+4 -1
View File
@@ -118,7 +118,10 @@ public:
// Shows/hides the native OrcaCloud pane beside this homepage nav column. `url` is the
// cloud Start URL ({web}/auth/handoff?next=…&user_id=…), which the page uses to decide
// whether it already has a session; empty falls back to the plain cloud page.
void set_cloud_mode(bool on, const wxString& url = wxString());
// `page_url` is a full {web} page URL: an already-open pane is navigated straight there,
// without re-running the handoff, which is what an explicit "open this page" wants. Empty
// keeps whatever page the pane holds.
void set_cloud_mode(bool on, const wxString& url = wxString(), const wxString& page_url = wxString());
// True while the OrcaCloud pane exists and is visible.
bool cloud_mode_active() const { return m_cloud_browser && m_cloud_browser->IsShown(); }
+1 -1
View File
@@ -32,7 +32,7 @@ END_EVENT_TABLE()
*/
#define TAB_BUTTON_SPACE 2
#define TAB_BUTTON_PADDING_X 2
#define TAB_BUTTON_PADDING_X 3
#define TAB_BUTTON_PADDING_Y 2
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
+27
View File
@@ -74,6 +74,10 @@ webkit_web_view_run_javascript_finish (WebKitWebView
GError **error);
WEBKIT_API void
webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
WEBKIT_API guint
webkit_get_major_version (void);
WEBKIT_API guint
webkit_get_minor_version (void);
}
#endif
@@ -427,6 +431,29 @@ bool WebView::NeedsRecreateOnShow()
return false;
#endif
}
bool WebView::NeedsCompositingWorkaround()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major < 2 || (major == 2 && minor < 42);
#else
return false;
#endif
}
bool WebView::SupportsForceSharedMemory()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major > 2 || (major == 2 && minor >= 44);
#else
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime()
{
+10
View File
@@ -34,6 +34,16 @@ public:
// ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow();
// True when the linked WebKitGTK predates 2.42, where accelerated compositing
// could fail silently on XWayland sessions, so it is forced off as a fallback.
// False on every other platform. Safe to call before GTK initialises.
static bool NeedsCompositingWorkaround();
// True when the linked WebKitGTK is new enough (>= 2.44) to honour
// WEBKIT_DMABUF_RENDERER_FORCE_SHM, the shared-memory transport switch.
// False on every other platform. Safe to call before GTK initialises.
static bool SupportsForceSharedMemory();
static void RecreateAll();
};
+1
View File
@@ -529,6 +529,7 @@ std::string PluginConfig::capability_type_label(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return _u8L("Visualization");
case PluginCapabilityType::Script: return _u8L("Script");
case PluginCapabilityType::SlicingPipeline: return _u8L("Slicing Pipeline");
case PluginCapabilityType::ResourceTexture: return _u8L("Resource Texture");
default: return _u8L("Unknown");
}
}
+79 -17
View File
@@ -9,6 +9,7 @@
#include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp>
#include <boost/log/trivial.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/nowide/fstream.hpp>
#include "PluginAuditManager.hpp"
@@ -33,10 +34,6 @@
#include <unordered_set>
#include <vector>
#ifdef WIN32
#include <boost/locale/encoding_utf.hpp>
#endif
namespace Slic3r {
const char* const INSTALL_STATE_FILE = ".install_state.json";
@@ -664,7 +661,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::create_directories(destination, ec);
fs::create_directories(extended_path(destination), ec);
if (ec) {
error = "Failed to create plugin staging directory: " + ec.message();
return false;
@@ -708,7 +705,7 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
const fs::path output_path = destination / relative_path;
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
fs::create_directories(output_path, ec);
fs::create_directories(extended_path(output_path), ec);
if (ec) {
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
return false;
@@ -716,26 +713,19 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
continue;
}
fs::create_directories(output_path.parent_path(), ec);
fs::create_directories(extended_path(output_path.parent_path()), ec);
if (ec) {
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
return false;
}
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
if (fs::exists(extended_path(output_path), ec) && fs::is_directory(extended_path(output_path), ec)) {
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
return false;
}
const std::string encoded_output_path = encode_path(output_path.string().c_str());
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
#ifdef WIN32
if (!extracted) {
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
}
#endif
if (!extracted) {
// Shared with the rest of the archive code: long-path capable and binary-safe.
if (!extract_zip_entry_to_file(&reader.archive, stat.m_file_index, output_path)) {
error = "Failed to extract plugin zip entry " + relative_key + ": " +
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
@@ -1255,4 +1245,76 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
return {};
}
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error)
{
namespace fs = boost::filesystem;
// Picker sanity bound, not a security control: the popup draws every entry in its grid.
constexpr size_t kMaxResourceTextureFiles = 64;
std::vector<std::string> files;
error.clear();
if (dir.empty()) {
error = _u8L("Resource texture directory is not set");
return files;
}
if (plugin_root.empty()) {
error = _u8L("Plugin folder could not be determined.");
return files;
}
boost::system::error_code ec;
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
if (ec) {
error = _u8L("Resource texture directory could not be resolved");
return files;
}
if (!fs::is_directory(resolved_dir, ec) || ec) {
error = _u8L("Resource texture path is not a directory");
return files;
}
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
if (ec) {
error = _u8L("Plugin folder could not be resolved");
return files;
}
if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
error = _u8L("Resource texture directory is outside the plugin folder");
return files;
}
for (fs::directory_iterator it(resolved_dir, ec), end; it != end && !ec; it.increment(ec)) {
const fs::path& path = it->path();
if (!fs::is_regular_file(path, ec) || ec)
continue;
std::string ext = path.extension().string();
boost::algorithm::to_lower(ext);
if (ext != ".png" && ext != ".jpg" && ext != ".jpeg" && ext != ".bmp")
continue;
// Re-check per file: a symlink inside the pack must not resolve outside it.
if (!is_inside_allowed_root(path, resolved_root))
continue;
files.push_back(path.string());
if (files.size() >= kMaxResourceTextureFiles)
break;
}
if (ec) {
error = _u8L("Resource texture directory could not be read");
return {};
}
return files;
}
} // namespace Slic3r
+6
View File
@@ -176,6 +176,12 @@ bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginD
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
// Validated non-recursive .png/.jpg/.jpeg/.bmp files directly inside `dir`, every one contained in
// `plugin_root`. Empty with `error` set on failure.
std::vector<std::string> collect_resource_texture_files(const std::string& dir,
const std::string& plugin_root,
std::string& error);
// Canonical writer: emits the .install_state.json schema for the given state.
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
+32
View File
@@ -9,6 +9,7 @@
#include "PluginFsUtils.hpp"
#include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "libslic3r/Utils.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
@@ -387,6 +388,37 @@ bool load(const PluginDescriptor& descriptor,
instance->set_config_ui_available(false);
}
// ResourceTexture capabilities resolve their texture directory here, under the
// GIL this block already holds, and cache the validated result. A resolution
// fault only costs this capability its textures, so it is caught locally like
// has_config_ui() above rather than failing the load.
if (auto* resource_texture = dynamic_cast<ResourceTexturePluginCapability*>(instance.get())) {
try {
const std::string texture_dir = resource_texture->get_texture_dir();
if (texture_dir.empty()) {
resource_texture->set_texture_dir({});
} else {
std::string texture_error;
collect_resource_texture_files(texture_dir, descriptor.plugin_root, texture_error);
if (texture_error.empty()) {
resource_texture->set_texture_dir(texture_dir);
} else {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': texture directory rejected (" << texture_error
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning)
<< "Plugin capability '" << found.name << "' of plugin '" << descriptor.plugin_key
<< "': get_texture_dir() failed (" << ex.what()
<< "); the capability will provide no textures";
resource_texture->set_texture_dir({});
}
}
capabilities.push_back(std::move(instance));
}
} catch (const std::exception& ex) {
+3
View File
@@ -25,6 +25,7 @@
#include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include <pybind11/pytypes.h>
@@ -374,6 +375,7 @@ void bind_python_api(pybind11::module_& m)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("ResourceTexture", PluginCapabilityType::ResourceTexture)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
@@ -527,6 +529,7 @@ void bind_python_api(pybind11::module_& m)
PagesPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m);
ResourceTexturePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
+5 -1
View File
@@ -14,7 +14,7 @@
namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, ResourceTexture, Unknown };
struct PluginCapabilityId
{
@@ -48,6 +48,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
case PluginCapabilityType::Visualization: return "visualization";
case PluginCapabilityType::Script: return "script";
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
case PluginCapabilityType::ResourceTexture: return "resource-texture";
default: return "unknown";
}
}
@@ -63,6 +64,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
case PluginCapabilityType::Visualization: return "Visualization";
case PluginCapabilityType::Script: return "Script";
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
case PluginCapabilityType::ResourceTexture: return "Resource Texture";
default: return "Unknown";
}
}
@@ -92,6 +94,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
return PluginCapabilityType::Script;
if (lowered == "slicing-pipeline")
return PluginCapabilityType::SlicingPipeline;
if (lowered == "resource-texture")
return PluginCapabilityType::ResourceTexture;
return PluginCapabilityType::Unknown;
}
+7
View File
@@ -4,6 +4,7 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/PluginManager.hpp"
@@ -382,6 +383,7 @@ void PluginPages::relayout()
m_parent->SelectPageByName(id_to_reselect);
}
const bool overflow_changed = need_overflow != (m_overflow_button != nullptr);
if (need_overflow) {
if (m_overflow_button == nullptr) {
auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER);
@@ -398,6 +400,11 @@ void PluginPages::relayout()
m_overflow_button->Destroy();
m_overflow_button = nullptr;
}
// The tab strip changed width; compact the labels again so the side tools stay visible.
if ((!up_to_date || overflow_changed) && !GUI::wxGetApp().is_closing())
if (GUI::MainFrame* mainframe = GUI::wxGetApp().mainframe)
mainframe->fit_tab_labels();
}
void PluginPages::select_page(const PluginCapabilityId& id)
@@ -0,0 +1,115 @@
#include "PluginResourceTextures.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
PluginResourceTextures &PluginResourceTextures::instance()
{
static PluginResourceTextures registry;
return registry;
}
std::optional<ResourceTextureEntry> PluginResourceTextures::build_entry(const PluginCapabilityId &id) const
{
// Query by id and re-check state; the manager copies the shared_ptr and releases its lock.
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
if (!capability || !capability->is_enabled() || capability->type() != PluginCapabilityType::ResourceTexture)
return std::nullopt;
auto texture_capability = std::dynamic_pointer_cast<ResourceTexturePluginCapability>(capability);
if (!texture_capability)
return std::nullopt;
// An empty directory means resolution failed at load, so the pack contributes nothing.
const std::string &texture_dir = texture_capability->texture_dir();
if (texture_dir.empty())
return std::nullopt;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor_for_capability(capability->name(),
PluginCapabilityType::ResourceTexture,
descriptor))
return std::nullopt;
ResourceTextureEntry entry;
entry.id = capability->identity();
entry.plugin_key = entry.id.plugin_key;
entry.display_name = capability->name();
entry.texture_dir = texture_dir;
entry.plugin_root = descriptor.plugin_root;
return entry;
}
void PluginResourceTextures::on_plugin_register(const std::string &plugin_key)
{
for (const auto &capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::ResourceTexture)) {
if (capability)
on_cap_register(capability->identity());
}
}
void PluginResourceTextures::on_plugin_deregister(const std::string &plugin_key)
{
std::lock_guard<std::mutex> lock(m_mutex);
for (auto it = m_entries.begin(); it != m_entries.end();) {
if (it->first.plugin_key == plugin_key)
it = m_entries.erase(it);
else
++it;
}
}
void PluginResourceTextures::on_cap_register(const PluginCapabilityId &id)
{
std::optional<ResourceTextureEntry> entry = build_entry(id);
std::lock_guard<std::mutex> lock(m_mutex);
if (entry)
m_entries[entry->id] = std::move(*entry);
}
void PluginResourceTextures::on_cap_deregister(const PluginCapabilityId &id)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_entries.erase(id);
}
void PluginResourceTextures::initial_sweep()
{
std::map<PluginCapabilityId, ResourceTextureEntry> entries;
for (const auto &capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::ResourceTexture)) {
if (!capability)
continue;
std::optional<ResourceTextureEntry> entry = build_entry(capability->identity());
if (entry)
entries[entry->id] = std::move(*entry);
}
std::lock_guard<std::mutex> lock(m_mutex);
m_entries = std::move(entries);
}
std::vector<ResourceTextureEntry> PluginResourceTextures::entries() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<ResourceTextureEntry> result;
result.reserve(m_entries.size());
for (const auto &entry : m_entries)
result.push_back(entry.second);
return result;
}
} // namespace Slic3r
@@ -0,0 +1,50 @@
#pragma once
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace Slic3r {
// One ResourceTexture capability contributing a texture directory to the texture picker.
struct ResourceTextureEntry
{
PluginCapabilityId id;
std::string plugin_key;
std::string display_name;
std::string texture_dir;
std::string plugin_root; // so a scan can re-check containment
};
// Tracks every loaded and enabled ResourceTexture capability by id. Holds strings only: a
// shared_ptr here would keep a Python object alive past Py_Finalize.
class PluginResourceTextures
{
public:
static PluginResourceTextures &instance();
PluginResourceTextures(const PluginResourceTextures &) = delete;
PluginResourceTextures &operator=(const PluginResourceTextures &) = delete;
void on_plugin_register(const std::string &plugin_key);
void on_plugin_deregister(const std::string &plugin_key);
void on_cap_register(const PluginCapabilityId &id);
void on_cap_deregister(const PluginCapabilityId &id);
void initial_sweep();
std::vector<ResourceTextureEntry> entries() const;
private:
PluginResourceTextures() = default;
std::optional<ResourceTextureEntry> build_entry(const PluginCapabilityId &id) const;
mutable std::mutex m_mutex;
std::map<PluginCapabilityId, ResourceTextureEntry> m_entries;
};
} // namespace Slic3r
@@ -0,0 +1,32 @@
#include "ResourceTexturePluginCapability.hpp"
#include "ResourceTexturePluginCapabilityTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <boost/log/trivial.hpp>
#include <memory>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
namespace py = pybind11;
namespace Slic3r {
void ResourceTexturePluginCapability::RegisterBindings(pybind11::module_& module)
{
BOOST_LOG_TRIVIAL(debug) << "Registering orca.resource_texture bindings";
auto resource_texture = module.def_submodule("resource_texture", "Plugin resource texture API");
py::class_<ResourceTexturePluginCapability, PluginCapabilityInterface,
PyResourceTexturePluginCapabilityTrampoline,
std::shared_ptr<ResourceTexturePluginCapability>>(resource_texture,
"ResourceTexturePluginCapabilityBase")
.def(py::init<>())
.def("get_type", &ResourceTexturePluginCapability::get_type)
.def("get_texture_dir", &ResourceTexturePluginCapability::get_texture_dir);
}
} // namespace Slic3r
@@ -0,0 +1,28 @@
#pragma once
#include "../../PythonPluginInterface.hpp"
#include "pybind11/pybind11.h"
#include <string>
#include <utility>
namespace Slic3r {
class ResourceTexturePluginCapability : public PluginCapabilityInterface
{
public:
static void RegisterBindings(pybind11::module_& module);
PluginCapabilityType get_type() const override { return PluginCapabilityType::ResourceTexture; }
// Python contract: the directory the host scans for textures. Resolved once at materialization
// under the GIL; never called from the GUI. Overridden in Python through the trampoline.
virtual std::string get_texture_dir() const = 0;
// C++-only read of the validated directory cached by the loader; empty if resolution failed.
const std::string& texture_dir() const { return m_texture_dir; }
void set_texture_dir(std::string dir) { m_texture_dir = std::move(dir); }
private:
std::string m_texture_dir;
};
} // namespace Slic3r
@@ -0,0 +1,30 @@
#pragma once
#include "ResourceTexturePluginCapability.hpp"
#include "../../PyPluginTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <string>
#include "slic3r/plugin/PythonPluginInterface.hpp"
namespace Slic3r {
class PyResourceTexturePluginCapabilityTrampoline :
public PyPluginCommonTrampoline<ResourceTexturePluginCapability>
{
public:
using PyPluginCommonTrampoline<ResourceTexturePluginCapability>::PyPluginCommonTrampoline;
std::string get_texture_dir() const override
{
ORCA_PY_OVERRIDE_AUDITED(
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
ResourceTexturePluginCapability,
get_texture_dir);
}
};
} // namespace Slic3r