Merge branch 'main' into feature/filament_id

This commit is contained in:
SoftFever
2026-08-21 17:30:12 +08:00
166 changed files with 13041 additions and 2723 deletions
+21 -8
View File
@@ -256,14 +256,6 @@ if (WIN32)
VERBATIM
)
endforeach ()
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_Release)
else()
orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release)
endif()
else ()
file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/resources" WIN_RESOURCES_SYMLINK)
add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -279,6 +271,27 @@ if (WIN32)
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
if (CMAKE_CONFIGURATION_TYPES)
# Multi-config generators (Visual Studio, Ninja Multi-Config): copy per config.
foreach (cfg ${CMAKE_CONFIGURATION_TYPES})
if ("${cfg}" STREQUAL "Debug")
orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
elseif("${cfg}" STREQUAL "RelWithDebInfo")
orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo)
else()
orcaslicer_copy_dlls(COPY_DLLS "${cfg}" "" output_dlls_${cfg})
endif()
endforeach()
else()
# Single-config generators (Ninja): use CMAKE_BUILD_TYPE.
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
orcaslicer_copy_dlls(COPY_DLLS "Debug" "d" output_dlls_Debug)
elseif("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
orcaslicer_copy_dlls(COPY_DLLS "RelWithDebInfo" "" output_dlls_RelWithDebInfo)
else()
orcaslicer_copy_dlls(COPY_DLLS "Release" "" output_dlls_Release)
endif()
endif()
else ()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
+1 -1
View File
@@ -69,7 +69,7 @@ void CBaseException::OutputString(LPCTSTR lpszFormat, ...)
//WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, _tcslen(szBuf), NULL, NULL);
//output it to the current directory of binary
std::string output_str = textconv_helper::T2A_(szBuf);
std::string output_str = static_cast<const char*>(textconv_helper::T2A_(szBuf));
*output_file << output_str;
output_file->flush();
}
+10
View File
@@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK)
)
endif()
if (ORCA_TOOLS)
set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
# generate_system_cache: pre-generates per-vendor <vendor>.opc files under resources/profiles for CI bundling.
add_executable(generate_system_cache generate_system_cache.cpp)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
endif()
# Function that adds source file encoding check to a target
# using the above encoding-check binary
+84
View File
@@ -0,0 +1,84 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/program_options.hpp>
#include <iostream>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace po = boost::program_options;
int main(int argc, char* argv[])
{
po::options_description desc("OrcaSlicer System Cache Generator\nUsage");
// clang-format off
desc.add_options()
("help,h", "Show help")
#ifdef __APPLE__
("path,p", po::value<std::string>()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory")
#else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
// clang-format on
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
if (vm.count("help")) { std::cout << desc << "\n"; return 0; }
po::notify(vm);
} catch (const po::error& e) {
std::cerr << "Error: " << e.what() << "\n" << desc << "\n";
return 1;
}
const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
return 1;
}
set_logging_level(log_level);
set_data_dir(profiles_path);
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
if (!fs::exists(user_dir))
fs::create_directories(user_dir);
AppConfig app_config;
app_config.set("preset_folder", "default");
auto preset_bundle = std::make_unique<PresetBundle>();
preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true);
std::cout << "Loading system presets from: " << profiles_path << "\n";
try {
// In validation mode data_dir() is the profiles directory set above, so the
// loader writes each <vendor>.opc next to its <vendor>.json as it parses it.
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
} catch (const std::exception& ex) {
std::cerr << "Failed to load presets: " << ex.what() << "\n";
return 1;
}
size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc"))
++ cache_count;
if (cache_count == 0) {
std::cerr << "No vendor cache files were generated under " << profiles_path << "\n";
return 1;
}
std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n";
return 0;
}
+25
View File
@@ -280,6 +280,9 @@ void AppConfig::set_defaults()
set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap));
}
// The getter already defaults, parses and clamps; write back what it resolves to.
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
@@ -853,6 +856,10 @@ std::string AppConfig::load()
local_machine.dev_ip = p["dev_ip"].get<std::string>();
if (p.contains("printer_type"))
local_machine.printer_type = p["printer_type"].get<std::string>();
if (p.contains("printer_agent_id"))
local_machine.printer_agent_id = p["printer_agent_id"].get<std::string>();
if (p.contains("access_code"))
local_machine.access_code = p["access_code"].get<std::string>();
m_local_machines[local_machine.dev_id] = local_machine;
}
} else {
@@ -1065,6 +1072,8 @@ void AppConfig::save()
m_json["dev_name"] = local_machine.second.dev_name;
m_json["dev_ip"] = local_machine.second.dev_ip;
m_json["printer_type"] = local_machine.second.printer_type;
m_json["printer_agent_id"] = local_machine.second.printer_agent_id;
m_json["access_code"] = local_machine.second.access_code;
j["local_machines"][local_machine.first] = m_json;
}
@@ -1630,6 +1639,22 @@ void AppConfig::set_network_plugin_version(const std::string& version)
set(SETTING_NETWORK_PLUGIN_VERSION, version);
}
int AppConfig::get_plugin_pages_visible_count() const
{
std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT);
if (value.empty())
return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
try {
visible_count = std::stoi(value);
}
catch (...) {
return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
}
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
}
std::vector<std::string> AppConfig::get_skipped_network_versions() const
{
std::vector<std::string> result;
+19 -1
View File
@@ -41,6 +41,11 @@ using namespace nlohmann;
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count"
#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
#if defined(_WIN32) || defined(_WIN64)
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
#else
@@ -61,10 +66,19 @@ struct BBLocalMachine
std::string dev_ip;
std::string dev_id; /* serial number */
std::string printer_type; /* model_id */
std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */
// Access code, scoped to printer_agent_id above - so a code saved while bound under one
// printer agent isn't treated as valid for a different, independent agent talking to the
// same physical dev_id. Empty for entries persisted before this field existed; those fall
// back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only,
// since BBL was the only agent when they were saved) - see
// get_access_code_with_legacy_fallback() in DevManager.cpp.
std::string access_code;
bool operator==(const BBLocalMachine& other) const
{
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type;
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type &&
printer_agent_id == other.printer_agent_id && access_code == other.access_code;
}
bool operator!=(const BBLocalMachine& other) const { return !operator==(other); }
};
@@ -374,6 +388,10 @@ public:
std::string get_network_plugin_version() const;
void set_network_plugin_version(const std::string& version);
// Number of plugin pages shown as fixed tabs before the rest are collapsed into a
// dropdown on the last tab.
int get_plugin_pages_visible_count() const;
std::vector<std::string> get_skipped_network_versions() const;
void add_skipped_network_version(const std::string& version);
bool is_network_version_skipped(const std::string& version) const;
+2
View File
@@ -8,6 +8,8 @@
namespace Slic3r {
template BoundingBoxBase<Point, Points>::BoundingBoxBase(const Points &points);
template void BoundingBoxBase<Point, Points>::construct<0, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
template void BoundingBoxBase<Point, Points>::construct<1, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
template BoundingBoxBase<Vec2d>::BoundingBoxBase(const std::vector<Vec2d> &points);
template BoundingBox3Base<Vec3d>::BoundingBox3Base(const std::vector<Vec3d> &points);
+4
View File
@@ -149,6 +149,8 @@ set(lisbslic3r_sources
Fill/FillConcentric.hpp
Fill/FillConcentricInternal.cpp
Fill/FillConcentricInternal.hpp
Fill/FillCornerSmoothing.cpp
Fill/FillCornerSmoothing.hpp
Fill/Fill.cpp
Fill/FillCrossHatch.cpp
Fill/FillCrossHatch.hpp
@@ -346,6 +348,8 @@ set(lisbslic3r_sources
Polyline.hpp
PresetBundle.cpp
PresetBundle.hpp
PresetCacheFormat.cpp
PresetCacheFormat.hpp
Preset.cpp
Preset.hpp
PrincipalComponents2D.cpp
+5
View File
@@ -28,6 +28,9 @@
#include <cereal/access.hpp>
#include <cereal/types/base_class.hpp>
// The serialize() members below archive ConfigOption hierarchies through
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
#include <cereal/types/polymorphic.hpp>
namespace Slic3r {
struct FloatOrPercent
@@ -2982,6 +2985,8 @@ public:
const double & opt_float(const t_config_option_key &opt_key, unsigned int idx) const;
double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsNullable>(opt_key)->get_at(idx); }
const double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsNullable *>(this->option(opt_key))->get_at(idx); }
FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsOrPercentsNullable>(opt_key)->get_at(idx); }
const FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(this->option(opt_key))->get_at(idx); }
int& opt_int(const t_config_option_key &opt_key) { return this->option<ConfigOptionInt>(opt_key)->value; }
int opt_int(const t_config_option_key &opt_key) const { return dynamic_cast<const ConfigOptionInt*>(this->option(opt_key))->value; }
+3 -3
View File
@@ -970,9 +970,9 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
region_config.sparse_infill_rotate_template.value);
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
// Orca: special case; apply smoothing factor only for Hilbert Curve sparse infill.
// FillHilbertCurve::generate clamps and validates the value itself.
if (params.pattern == ipHilbertCurve)
// Orca: the smoothing factor only applies to the sparse infill patterns that
// implement it. The fills clamp and validate the value themselves.
if (is_smoothable_infill_pattern(params.pattern, params.multiline))
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
} else {
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
+4
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp"
#include "../Surface.hpp"
#include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "Fill3DHoneycomb.hpp"
namespace Slic3r {
@@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single(
for (Polyline &pl : polylines){
pl.translate(bb.min);
pl.simplify(5 * spacing); // simplify to 5x line width
// Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a
// straight line have no corner to round.
smooth_polyline_corners(pl, params.smooth_factor, scaled<double>(params.resolution));
}
// Apply multiline offset if needed
+22 -1
View File
@@ -5,6 +5,7 @@
#include "Arachne/WallToolPaths.hpp"
#include "FillConcentric.hpp"
#include "FillCornerSmoothing.hpp"
#include <libslic3r/ShortestPath.hpp>
namespace Slic3r {
@@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single(
Polygons loops = to_polygons(contracted);
ExPolygons last { std::move(contracted) };
ExPolygons last { contracted };
while (! last.empty()) {
last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2);
append(loops, to_polygons(last));
}
// Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the
// fill region - they are its offsets - so a corner may only be rounded where the curve replacing it
// stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave
// feature or across a thin region is outside the fill and would put the extrusion over a wall.
// The reach is capped at half the distance between two loops as well: a loop is as long as the
// object, and a corner cut by half of its side would swallow the neighbouring loops.
auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) {
// The straight chord between the ends of the curve is the deepest the curve can cut.
for (const double t : { 0.25, 0.5, 0.75 }) {
const Vec2d sample = from + t * (to - from);
const Point point(coord_t(sample.x()), coord_t(sample.y()));
if (std::none_of(contracted.begin(), contracted.end(),
[&point](const ExPolygon &region) { return region.contains(point); }))
return false;
}
return true;
};
smooth_polygons_corners(loops, params.smooth_factor, scaled<double>(params.resolution), 0.5 * distance,
corner_stays_inside);
// generate paths from the outermost to the innermost, to avoid
// adhesion problems of the first central tiny loops
loops = union_pt_chained_outside_in(loops);
+226
View File
@@ -0,0 +1,226 @@
#include <array>
#include "FillCornerSmoothing.hpp"
namespace Slic3r {
// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly
// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin.
static constexpr const double min_smoothed_turn_cosine = -0.9;
// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not
// orthonormal for turns other than a right angle.
using QuinticBezier = std::array<Vec2d, 6>;
static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation)
{
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
// comparing squared values avoids a square root.
auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; };
const Vec2d chord = in_plane(curve.back() - curve.front());
const double chord_length_sq = chord.squaredNorm();
const double max_cross_sq = deviation * deviation * chord_length_sq;
for (size_t i = 1; i + 1 < curve.size(); ++i) {
const Vec2d offset = in_plane(curve[i] - curve.front());
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
if (cross * cross > max_cross_sq)
return false;
}
return true;
}
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
{
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
// control point to the left half and one to the right half; the latter is filled backwards to keep
// both resulting control polygons in their original parameter direction.
QuinticBezier subdivision = curve;
left.front() = subdivision.front();
right.back() = subdivision.back();
for (size_t level = 1; level < curve.size(); ++level) {
for (size_t i = 0; i + level < curve.size(); ++i)
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
left[level] = subdivision.front();
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
}
}
static void flatten_bezier(
const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector<Vec2d> &output)
{
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
static constexpr size_t max_depth = 16;
std::vector<QuinticBezier> subcurves(2);
subdivide_bezier(curve, subcurves[0], subcurves[1]);
for (size_t depth = 1; depth < max_depth; ++depth) {
bool all_flat = true;
for (const QuinticBezier &c : subcurves)
if (!is_bezier_flat(c, incoming, outgoing, deviation)) {
all_flat = false;
break;
}
if (all_flat)
break;
std::vector<QuinticBezier> finer(subcurves.size() * 2);
for (size_t i = 0; i < subcurves.size(); ++i)
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
subcurves = std::move(finer);
}
// The curve start is deliberately omitted so it can be shared with the straight leg feeding into it.
output.clear();
output.reserve(subcurves.size());
for (const QuinticBezier &c : subcurves)
output.emplace_back(c.back());
}
const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing)
{
const double cosine = incoming.dot(outgoing);
// Corners of the same size and turn angle are congruent, so they flatten identically. An infill
// path walks over the very same corner over and over again, the Hilbert curve over a single one.
if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine)
return m_cached_coefficients;
// One canonical corner running from -corner_distance along the incoming leg to corner_distance
// along the outgoing one. At each end, the first three control points are collinear and equally
// spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The
// endpoint curvature is therefore zero, giving G2 joins to both legs.
const double d = corner_distance;
const QuinticBezier corner_curve {{
{-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d}
}};
// Retain a finite positive tolerance if the smoother was set up with an invalid one.
const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON;
flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients);
m_cached_distance = corner_distance;
m_cached_cosine = cosine;
m_has_cached_coefficients = true;
return m_cached_coefficients;
}
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
{
m_corner_points.clear();
const Vec2d incoming_leg = corner - previous;
const Vec2d outgoing_leg = next - corner;
const double incoming_length = incoming_leg.norm();
const double outgoing_length = outgoing_leg.norm();
if (incoming_length < EPSILON || outgoing_length < EPSILON) {
m_corner_points.emplace_back(corner);
return;
}
const Vec2d incoming = incoming_leg / incoming_length;
const Vec2d outgoing = outgoing_leg / outgoing_length;
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
// A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above.
if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) {
m_corner_points.emplace_back(corner);
return;
}
// Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart.
double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length);
if (m_max_corner_distance > 0.)
corner_distance = std::min(corner_distance, m_max_corner_distance);
const Vec2d curve_start = corner - corner_distance * incoming;
const Vec2d curve_end = corner + corner_distance * outgoing;
if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) {
m_corner_points.emplace_back(corner);
return;
}
const std::vector<Vec2d> &coefficients = curve_coefficients(corner_distance, incoming, outgoing);
m_corner_points.reserve(coefficients.size() + 1);
m_corner_points.emplace_back(curve_start);
for (const Vec2d &coefficient : coefficients)
m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
}
// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices
// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a
// path returning to where it started retraces its way back and is not a loop.
static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother)
{
// A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes
// feeding the smoother the last vertex first, whose own output point is then dropped again.
size_t skip = polygon ? 1 : 0;
Points smoothed;
smoothed.reserve(2 * points.size());
auto emit = [&smoothed, &skip](const Vec2d &point) {
if (skip > 0) {
--skip;
return;
}
smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5)));
};
if (polygon)
smoother.push(points.back().cast<double>(), emit);
for (const Point &point : points)
smoother.push(point.cast<double>(), emit);
if (polygon)
// Wrap the first vertex around, so that the last one is a corner as well.
smoother.push(points.front().cast<double>(), emit);
smoother.flush(emit);
if (polygon)
// The flushed point is the wrapped first vertex, which a polygon does not store.
smoothed.pop_back();
return smoothed;
}
void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
if (!smoother.enabled() || polyline.size() < 3)
return;
polyline.points = smooth_corners(polyline.points, false, smoother);
// Rounding back to the integer grid may collapse neighbouring samples of a curve.
polyline.remove_duplicate_points();
}
void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
if (sanitize_smooth_factor(smooth_factor) == 0.)
return;
for (Polyline &polyline : polylines)
smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter);
}
void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance,
const double max_corner_distance, const CornerFilter &corner_filter)
{
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
if (!smoother.enabled())
return;
for (Polygon &polygon : polygons) {
if (polygon.size() < 3)
continue;
polygon.points = smooth_corners(polygon.points, true, smoother);
polygon.remove_duplicate_points();
// The curves of the first and of the last corner may have met on the segment they share. A
// polygon closes implicitly, so it must not repeat its first vertex at the end.
if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back())
polygon.points.pop_back();
}
}
} // namespace Slic3r
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <functional>
#include <vector>
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../Polygon.hpp"
#include "../Polyline.hpp"
namespace Slic3r {
// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>.
inline double sanitize_smooth_factor(double smooth_factor)
{
return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.;
}
// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it
// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the
// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it.
using CornerFilter = std::function<bool(const Vec2d &from, const Vec2d &to)>;
// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight
// legs with a continuous curvature, so the toolhead does not have to stop in every corner.
// Points are pushed one by one, because the plane path fills produce their path on the fly, and
// every point of the smoothed path is handed over to the caller supplied emit callback.
// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted
// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such
// duplicates is left to the caller, which is the only one knowing that grid.
class CornerSmoother
{
public:
// tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed
// points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it
// bounds how far a rounded corner moves away from the original path, which matters where the legs
// are much longer than the spacing of the pattern. Zero leaves the reach uncapped.
CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0.,
CornerFilter corner_filter = {})
: m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance),
m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter))
{}
bool enabled() const { return m_corner_distance_ratio > 0.; }
template<typename Emit> void push(const Vec2d &point, Emit &emit)
{
if (m_pending == 0) {
emit(point);
m_previous = point;
} else if (m_pending > 1) {
round_corner(m_previous, m_corner, point);
for (const Vec2d &corner_point : m_corner_points)
emit(corner_point);
m_previous = m_corner;
}
m_corner = point;
m_pending = std::min(m_pending + 1, 2);
}
// Emits the last point of the path and prepares the smoother for a new one.
template<typename Emit> void flush(Emit &emit)
{
if (m_pending > 1)
emit(m_corner);
m_pending = 0;
}
private:
// Fills m_corner_points with the points replacing the corner vertex.
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
// Flattens the canonical corner curve of the given size and turn into coordinates of the
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
// Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment
// is the maximum, otherwise the curves of two adjacent corners would overlap.
const double m_corner_distance_ratio;
const double m_tolerance;
const double m_max_corner_distance;
const CornerFilter m_corner_filter;
std::vector<Vec2d> m_corner_points;
// Cached flattening of the last corner, valid for corners of the same size and turn angle.
std::vector<Vec2d> m_cached_coefficients;
double m_cached_distance { 0. };
double m_cached_cosine { 0. };
bool m_has_cached_coefficients { false };
Vec2d m_previous { Vec2d::Zero() };
Vec2d m_corner { Vec2d::Zero() };
// Number of points held back: none, the first point of a path, or a corner candidate.
int m_pending { 0 };
};
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its
// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance.
void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance,
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance,
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
// Polygons close implicitly, so every one of their vertices is a corner.
void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance,
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
} // namespace Slic3r
+4
View File
@@ -3,6 +3,7 @@
#include "../Surface.hpp"
#include <cmath>
#include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillCrossHatch.hpp"
namespace Slic3r {
@@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single(
// shift the pattern to the actual space
for (Polyline &pl : polylines) { pl.translate(bb.min); }
// Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are.
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
+4
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp"
#include "../Surface.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillHoneycomb.hpp"
namespace Slic3r {
@@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single(
}
p.rotate(-direction.first, m.hex_center);
p.simplify(5 * spacing); // simplify to 5x line width
// Orca: round the corners of the honeycomb cells. Done before the clipping, so that the
// curves are cut by the region boundary just like the sharp path would be.
smooth_polyline_corners(p, params.smooth_factor, scaled<double>(params.resolution));
all_polylines.push_back(p);
}
}
+14
View File
@@ -2,6 +2,7 @@
#include "../Print.hpp"
#include "../ShortestPath.hpp"
#include "FillBase.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillLightning.hpp"
#include "Lightning/Generator.hpp"
@@ -17,6 +18,19 @@ void Filler::_fill_surface_single(
const Layer &layer = generator->getTreesForLayer(this->layer_id);
Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled<coord_t>(0.5 * this->spacing - this->overlap));
// Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and
// the reach is capped: cutting a corner moves the branch, and a branch is as long as the object
// rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour
// instead of rounding the turn between them. Half the distance between two branches keeps them
// apart. With more than one line per infill wall the branches are printed as outlines drawn around
// them, and the outlines of branches that run into each other merge into a single one; moving a
// branch by more than a fraction of its printed width breaks such an outline up into separate
// loops, so that width bounds the reach as well.
const double branch_width = scaled<double>(this->spacing) * params.multiline;
const double branch_spacing = branch_width / std::max(double(params.density), EPSILON);
const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing);
smooth_polylines_corners(fill_lines, params.smooth_factor, scaled<double>(params.resolution), max_reach);
// Apply multiline offset if needed
multiline_fill(fill_lines, params, spacing);
+59 -147
View File
@@ -2,6 +2,7 @@
#include "../ShortestPath.hpp"
#include "../Surface.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillPlanePath.hpp"
namespace Slic3r {
@@ -288,145 +289,60 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
}
}
using QuinticBezier = std::array<Vec2d, 6>;
static bool is_bezier_flat(const QuinticBezier &curve, const double deviation)
{
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
// comparing squared values avoids a square root.
const Vec2d chord = curve.back() - curve.front();
const double chord_length_sq = chord.squaredNorm();
const double max_cross_sq = deviation * deviation * chord_length_sq;
for (size_t i = 1; i + 1 < curve.size(); ++i) {
const Vec2d offset = curve[i] - curve.front();
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
if (cross * cross > max_cross_sq)
return false;
}
return true;
}
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
{
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
// control point to the left half and one to the right half; the latter is filled backwards to keep
// both resulting control polygons in their original parameter direction.
QuinticBezier subdivision = curve;
left.front() = subdivision.front();
right.back() = subdivision.back();
for (size_t level = 1; level < curve.size(); ++level) {
for (size_t i = 0; i + level < curve.size(); ++i)
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
left[level] = subdivision.front();
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
}
}
static void flatten_bezier(const QuinticBezier &curve, const double deviation, std::vector<Vec2d> &output)
{
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
static constexpr size_t max_depth = 16;
std::vector<QuinticBezier> subcurves(2);
subdivide_bezier(curve, subcurves[0], subcurves[1]);
for (size_t depth = 1; depth < max_depth; ++depth) {
bool all_flat = true;
for (const QuinticBezier &c : subcurves)
if (!is_bezier_flat(c, deviation)) {
all_flat = false;
break;
}
if (all_flat)
break;
std::vector<QuinticBezier> finer(subcurves.size() * 2);
for (size_t i = 0; i < subcurves.size(); ++i)
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
subcurves = std::move(finer);
}
// The curve start is deliberately omitted so consecutive curve pieces can share it without duplication.
output.reserve(output.size() + subcurves.size());
for (const QuinticBezier &c : subcurves)
output.emplace_back(c.back());
}
// Rounds the corners of the generated path on its way to the infill output.
template<typename Output>
static void generate_smooth_hilbert_curve(
coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const double corner_distance, Output &output)
class SmoothingPolylineOutput
{
// A Hilbert curve is defined on a square grid whose side is a power of two. As in the unsmoothed
// generator, expand the larger requested dimension to the next valid Hilbert grid size. The output
// clipper or the later region intersection removes the padded part of the traversal.
size_t sz = 2;
const size_t sz0 = std::max(max_x + 1 - min_x, max_y + 1 - min_y);
while (sz < sz0)
sz <<= 1;
public:
SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance)
: m_output(output), m_smoother(smooth_factor, tolerance) {}
const size_t point_count = sz * sz;
output.reserve(point_count);
void reserve(size_t n) { m_output.reserve(n); }
void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); }
// The smoother holds back the last point of the path until it knows there is no corner left to round.
void finish() { auto emit = emitter(); m_smoother.flush(emit); }
// The caller normalizes resolution to the unit Hilbert grid; retain a finite positive tolerance
// if this helper is invoked with an invalid resolution.
const double deviation = resolution > 0. && std::isfinite(resolution) ? resolution : EPSILON;
// Construct one canonical 90-degree corner from (-corner_distance, 0) to (0, corner_distance).
// At each end, the first three control points are collinear and equally spaced: the tangent follows
// the adjoining straight leg and the second derivative is zero. The endpoint curvature is therefore
// zero, giving G2 joins to both legs. Every Hilbert turn is an oriented copy of this curve, so flatten
// it only once to the requested chordal-deviation tolerance.
const QuinticBezier corner_curve {{
{-corner_distance, 0.}, {-0.7 * corner_distance, 0.}, {-0.4 * corner_distance, 0.},
{0., 0.4 * corner_distance}, {0., 0.7 * corner_distance}, {0., corner_distance}
}};
std::vector<Vec2d> curve_coefficients;
flatten_bezier(corner_curve, deviation, curve_coefficients);
auto translated_point = [min_x, min_y](size_t idx) {
Point p = hilbert_n_to_xy(idx);
return Point(p.x() + min_x, p.y() + min_y);
};
auto to_vec2d = [](const Point &p) { return Vec2d(double(p.x()), double(p.y())); };
bool has_last_output = false;
Vec2d last_output;
// Fully smoothed adjacent corners may meet at the same segment midpoint. Suppress such duplicates
// to avoid emitting zero-length extrusion segments.
auto add_point = [&output, &has_last_output, &last_output](const Vec2d &point) {
if (!has_last_output || point.x() != last_output.x() || point.y() != last_output.y()) {
output.add_point(point);
last_output = point;
has_last_output = true;
}
};
Vec2d previous = to_vec2d(translated_point(0));
Vec2d corner = to_vec2d(translated_point(1));
add_point(previous);
// Replace each non-collinear Hilbert vertex by the canonical curve expressed in the local basis of
// its incoming and outgoing unit vectors. Collinear vertices remain part of the straight polyline.
for (size_t i = 1; i + 1 < point_count; ++i) {
const Vec2d next = to_vec2d(translated_point(i + 1));
const Vec2d incoming = (corner - previous).normalized();
const Vec2d outgoing = (next - corner).normalized();
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
if (std::abs(cross) < EPSILON) {
add_point(corner);
} else {
add_point(corner - corner_distance * incoming);
for (const Vec2d &coefficient : curve_coefficients)
add_point(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
}
previous = corner;
corner = next;
private:
// The curves of two adjacent corners meet at the midpoint of the segment they share, where they
// may round to the very same output point. Drop those, they would be zero length extrusions.
auto emitter()
{
return [this](const Vec2d &pt) {
const Point snapped = m_output.scaled(pt);
if (m_has_last_snapped && snapped == m_last_snapped)
return;
m_last_snapped = snapped;
m_has_last_snapped = true;
m_output.add_point(pt);
};
}
add_point(corner);
Output &m_output;
CornerSmoother m_smoother;
Point m_last_snapped { Point::Zero() };
bool m_has_last_snapped { false };
};
// Runs the path generator against the concrete output type, optionally through the corner smoother.
// The outputs do not share a virtual add_point(), so the type has to be resolved here.
template<typename GenerateFn>
static void generate_path(InfillPolylineOutput &output, const FillParams &params, const double resolution, GenerateFn generate)
{
const double smooth_factor = sanitize_smooth_factor(params.smooth_factor);
auto run = [smooth_factor, resolution, &generate](auto &out) {
if (smooth_factor == 0.) {
generate(out);
} else {
SmoothingPolylineOutput<std::remove_reference_t<decltype(out)>> smoothing(out, smooth_factor, resolution);
generate(smoothing);
smoothing.finish();
}
};
if (output.clips())
run(static_cast<InfillPolylineClipper&>(output));
else
run(output);
}
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
@@ -440,19 +356,8 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output)
{
const double smooth_factor = std::isfinite(params.smooth_factor) ?
std::clamp(params.smooth_factor, 0., 1.) : 0.;
if (smooth_factor == 0.) {
this->generate(min_x, min_y, max_x, max_y, resolution, output);
return;
}
const double corner_distance = 0.5 * smooth_factor;
if (output.clips())
generate_smooth_hilbert_curve(
min_x, min_y, max_x, max_y, resolution, corner_distance, static_cast<InfillPolylineClipper&>(output));
else
generate_smooth_hilbert_curve(min_x, min_y, max_x, max_y, resolution, corner_distance, output);
generate_path(output, params, resolution,
[min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); });
}
template<typename Output>
@@ -495,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c
generate_octagram_spiral(min_x, min_y, max_x, max_y, output);
}
void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output)
{
generate_path(output, params, resolution,
[min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); });
}
} // namespace Slic3r
+4 -2
View File
@@ -21,10 +21,10 @@ public:
void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); }
Points&& result() { return std::move(m_out); }
virtual bool clips() const { return false; }
protected:
// The output grid the generated points are snapped to.
const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; }
protected:
// Output polyline.
Points m_out;
@@ -93,6 +93,8 @@ public:
protected:
bool centered() const override { return true; }
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
const FillParams &params, InfillPolylineOutput &output) override;
};
} // namespace Slic3r
+5
View File
@@ -18,6 +18,7 @@
#include "../ShortestPath.hpp"
#include "../VariableWidth.hpp"
#include "FillCornerSmoothing.hpp"
#include "FillRectilinear.hpp"
// #define SLIC3R_DEBUG
@@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
for (Polyline &pl : polylines)
pl.translate(rotate_vector.second);
// Orca: round the corners of the trapezoids. The straight base lines of the triangular family
// have no corner to round.
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
// Apply multiline fill
multiline_fill(polylines, params, spacing);
+1
View File
@@ -6726,6 +6726,7 @@ void GCode::append_full_config(const Print &print, std::string &str)
"farthest_point_timelapse"sv,
"compatible_printers"sv,
"compatible_prints"sv,
"filament_colour_type"sv,
"print_host"sv,
"print_host_webui"sv,
"printhost_apikey"sv,
+127 -21
View File
@@ -19,6 +19,7 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <limits>
#include <numeric>
#include <unordered_map>
@@ -39,7 +40,11 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
const AABBTreeLines::LinesDistancer<L>& unscaled_prev_layer,
float flow_width,
float max_line_length = -1.0f,
float min_distance = -1.0f)
float min_distance = -1.0f,
// Maps an overhang distance onto the speed it will be printed at. Interior sampling
// needs it to tell which of the points it could add would change the G-code, and is
// skipped without it.
const std::function<float(float)>& distance_to_speed = {})
{
bool looped = input_points.front() == input_points.back();
std::function<size_t(size_t,size_t)> get_prev_index = [](size_t idx, size_t count) {
@@ -120,6 +125,107 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
points.push_back(next_point);
}
// ORCA: Interior sampling
// The passes below infer the support under a span from its endpoints alone, so an interior that is supported
// differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by
// full height walls reads as supported along its whole length. Probe the interior, keep the samples the
// endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly
// unsupported gets points where its support actually changes instead of one reading spread across all of it.
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) {
// Probe at least this densely before treating matching samples as evidence that a span is uniform. The
// segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer
// together than min_spacing, so finer discovery would not produce a more precise speed transition.
const double max_probe_spacing = std::max(2., 4. * min_spacing);
// A backstop for that length test, which on a non-finite length would never be met.
constexpr int max_bisection_depth = 10;
// Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends
// read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever
// its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections
// interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart.
// The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all.
auto same_speed = [&distance_to_speed](float a, float b) {
return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f;
};
// Whether the first reading is printed slower than the second, once they are known to differ.
auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); };
// Part of a segment still to bisect: its positions along the segment and bisections left.
struct Subspan { double t0, t1; int depth; };
std::vector<ExtendedPoint<L::Dim>> sampled_points; // Populated lazily, on the first insertion
std::vector<std::pair<double, float>> interior; // Samples of one segment, keyed by position along it
std::vector<Subspan> pending;
for (size_t point_idx = 0; point_idx + 1 < points.size(); ++point_idx) {
const ExtendedPoint<L::Dim>& curr = points[point_idx];
const ExtendedPoint<L::Dim>& next = points[point_idx + 1];
const Vec step = next.position - curr.position;
const double line_len = step.norm();
interior.clear();
if (line_len >= max_probe_spacing)
pending.push_back({0., 1., max_bisection_depth});
while (!pending.empty()) {
const Subspan subspan = pending.back();
pending.pop_back();
if (subspan.depth <= 0 || (subspan.t1 - subspan.t0) * line_len < max_probe_spacing)
continue;
const double t = 0.5 * (subspan.t0 + subspan.t1);
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
(curr.position + t * step).template cast<AABBScalar>());
const float sampled = float(distance + boundary_offset);
interior.emplace_back(t, sampled);
pending.push_back({subspan.t0, t, subspan.depth - 1});
pending.push_back({t, subspan.t1, subspan.depth - 1});
}
if (!interior.empty()) {
std::sort(interior.begin(), interior.end(),
[](const std::pair<double, float>& l, const std::pair<double, float>& r) { return l.first < r.first; });
// Coarse probing keeps every sample it took until this pass can see which ones bracket a speed
// transition. Matching samples cannot be discarded during discovery: one may be the last
// supported point before a narrow unsupported pocket found by a later probe.
size_t kept = 0;
for (size_t i = 0; i < interior.size(); ++i) {
const float sample = interior[i].second;
const bool at_start = kept == 0; // Nothing kept yet, so the segment's own start precedes it
const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end
const float before = at_start ? curr.distance : interior[kept - 1].second;
const float after = at_end ? next.distance : interior[i + 1].second;
// A sample is worth a point in the path only where it prints at a different speed from the
// readings either side of it. Differing from one of the segment's own ends is not enough on
// its own where the sample is the faster of the two: the segmentation pass below already
// ends the slowdown an end reads, at a distance taken from how far out that end is rather
// than from wherever bisection happened to stop, and a point here would leave the span
// beside the end too short for that pass to run at all. Support an end cannot account for,
// where the interior is the slower reading, is exactly what this pass is here to find.
const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before));
const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after));
if (worth_before || worth_after)
interior[kept++] = interior[i];
}
interior.resize(kept);
}
if (!interior.empty() && sampled_points.empty()) {
sampled_points.reserve(points.size() + 8);
sampled_points.assign(points.begin(), points.begin() + point_idx + 1);
}
if (!sampled_points.empty()) {
// Only a sub-span of max_probe_spacing or more is ever bisected, so these sit at least
// 2 * min_spacing apart, and need none of the filtering the passes either side of this one do.
for (const auto& [t, distance] : interior)
sampled_points.push_back({curr.position + t * step, distance});
sampled_points.push_back(next);
}
}
if (!sampled_points.empty())
points = std::move(sampled_points);
}
// Segmentation handling
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) {
std::vector<ExtendedPoint<L::Dim>> new_points;
@@ -362,9 +468,28 @@ public:
smallest_distance_with_lower_speed=-1.f;
// Orca: Pass to the point properties estimator the smallest ovehang distance that triggers a slowdown (smallest_distance_with_lower_speed)
auto calculate_speed = [&speed_sections, &original_speed](float distance) {
float final_speed;
if (distance <= speed_sections.front().first) {
final_speed = original_speed;
} else if (distance >= speed_sections.back().first) {
final_speed = speed_sections.back().second;
} else {
size_t section_idx = 0;
while (distance > speed_sections[section_idx + 1].first) {
section_idx++;
}
float t = (distance - speed_sections[section_idx].first) /
(speed_sections[section_idx + 1].first - speed_sections[section_idx].first);
t = std::clamp(t, 0.0f, 1.0f);
final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second;
}
return round(final_speed);
};
std::vector<ExtendedPoint<3>> extended_points =
estimate_points_properties<true, true, true, true>(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1,
smallest_distance_with_lower_speed);
smallest_distance_with_lower_speed, calculate_speed);
const auto width_inv = 1.0f / path.width;
std::vector<ProcessedPoint> processed_points;
processed_points.reserve(extended_points.size());
@@ -423,25 +548,6 @@ public:
}
}
auto calculate_speed = [&speed_sections, &original_speed](float distance) {
float final_speed;
if (distance <= speed_sections.front().first) {
final_speed = original_speed;
} else if (distance >= speed_sections.back().first) {
final_speed = speed_sections.back().second;
} else {
size_t section_idx = 0;
while (distance > speed_sections[section_idx + 1].first) {
section_idx++;
}
float t = (distance - speed_sections[section_idx].first) /
(speed_sections[section_idx + 1].first - speed_sections[section_idx].first);
t = std::clamp(t, 0.0f, 1.0f);
final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second;
}
return round(final_speed);
};
float extrusion_speed = std::min(calculate_speed(curr.distance), calculate_speed(next.distance));
// ORCA: Clamp resulting speed to lowest of calculated speed based on the overhang values and the current speed
// Fixes bug where resulting overhang speed is higher than the current speed due to (for example) volumetric flow limits.
+134 -20
View File
@@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset()
//BBS
enter_direction = { 0.0f, 0.0f, 0.0f };
exit_direction = { 0.0f, 0.0f, 0.0f };
jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f };
}
void GCodeProcessor::TimeMachine::CustomGCodeTime::reset()
@@ -5036,6 +5037,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
if (!is_extrusion_only_move(delta_pos))
curr.enter_direction = curr.enter_direction / norm;
curr.exit_direction = curr.enter_direction;
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
static_cast<float>(delta_pos[Y]),
static_cast<float>(delta_pos[Z]),
static_cast<float>(delta_pos[E])).normalized();
TimeBlock block;
block.move_type = type;
@@ -5118,22 +5123,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
block.acceleration = acceleration;
// calculates block exit feedrate
curr.safe_feedrate = block.feedrate_profile.cruise;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
// Negative leaves the classic jerk path below unchanged.
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
// free to start from rest.
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
if (!use_junction_deviation) {
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
}
}
block.feedrate_profile.exit = curr.safe_feedrate;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
// calculates block entry feedrate
float vmax_junction = curr.safe_feedrate;
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
if (!use_junction_deviation && has_prev_move) {
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
@@ -5400,6 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
if (!is_extrusion_only_move(delta_pos))
curr.enter_direction = curr.enter_direction / norm;
curr.exit_direction = curr.enter_direction;
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
static_cast<float>(delta_pos[Y]),
static_cast<float>(delta_pos[Z]),
static_cast<float>(delta_pos[E])).normalized();
TimeBlock block;
block.move_type = type;
@@ -5480,22 +5499,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
block.acceleration = acceleration;
// calculates block exit feedrate
curr.safe_feedrate = block.feedrate_profile.cruise;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
// Negative leaves the classic jerk path below unchanged.
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
// free to start from rest.
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
if (!use_junction_deviation) {
for (unsigned char a = X; a <= E; ++a) {
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
}
}
block.feedrate_profile.exit = curr.safe_feedrate;
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
// calculates block entry feedrate
float vmax_junction = curr.safe_feedrate;
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
if (!use_junction_deviation && has_prev_move) {
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
@@ -7168,6 +7197,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM
return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode));
}
float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const
{
const size_t id = static_cast<size_t>(mode);
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
if (m_flavor == gcfKlipper) {
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
if (scv <= 0.0f || acceleration <= 0.0f)
return 0.0f;
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
}
// Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0.
if (m_flavor == gcfMarlinFirmware)
return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id);
return 0.0f;
}
float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
PrintEstimatedStatistics::ETimeMode mode) const
{
float junction_acceleration = block.acceleration;
for (unsigned char a = X; a <= E; ++a) {
if (junction_unit_vec[a] == 0.0f)
continue;
const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast<Axis>(a), m_machine_config_idx);
if (axis_max_acceleration > 0.0f)
junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a]));
}
return junction_acceleration;
}
// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp).
float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const
{
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
if (junction_deviation <= 0.0f)
return -1.0f; // classic jerk machine, the caller keeps its own computation
if (!has_prev_move)
return 0.0f; // starts from rest, the planner raises this on the reverse pass
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
if (junction_cos_theta > 0.999999f)
return 0.0f; // the path doubles back, the machine has to stop
junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below
const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive
const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec;
const float junction_vec_norm = junction_vec.norm();
const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm)
: Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode);
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
const float t = neg * junction_cos_theta;
const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f +
t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f))))));
const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033
vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta);
}
// Never faster than either of the two moves the junction joins.
vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate)));
return std::sqrt(vmax_junction_sqr);
}
float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const
{
const size_t id = static_cast<size_t>(mode);
+13
View File
@@ -637,6 +637,9 @@ class Print;
//For line move, there are same. For arc move, there are different.
Vec3f enter_direction;
Vec3f exit_direction;
// Orca: move direction over all four axes, unit length. Used by
// calc_vmax_junction_deviation(); see there for why E is normalized in.
Vec4f jd_unit_vec;
void reset();
};
@@ -1488,6 +1491,16 @@ class Print;
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const;
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
// Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine.
float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const;
// Orca: acceleration along the junction direction, clamped by the per axis limits.
float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
PrintEstimatedStatistics::ETimeMode mode) const;
// Orca: entry speed from the junction deviation model, which limits a corner by its angle alone
// and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead.
float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const;
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
+2 -2
View File
@@ -3243,9 +3243,9 @@ double Model::findMaxSpeed(const ModelObject* object) {
if (objectKey == "outer_wall_speed")
externalPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
if (objectKey == "small_perimeter_speed")
smallPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
smallPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(externalPerimeterSpeedObj);
if (objectKey == "small_support_perimeter_speed")
smallSupportPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
smallSupportPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(supportSpeedObj);
}
objMaxSpeed = std::max(perimeterSpeedObj, std::max(externalPerimeterSpeedObj, std::max(infillSpeedObj, std::max(solidInfillSpeedObj, std::max(topSolidInfillSpeedObj, std::max(supportSpeedObj, std::max(smallPerimeterSpeedObj, std::max(smallSupportPerimeterSpeedObj, objMaxSpeed))))))));
if (objMaxSpeed <= 0) objMaxSpeed = 250.;
+24 -11
View File
@@ -147,6 +147,9 @@ Semver get_version_from_json(std::string file_path)
return Semver();
//throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what()));
}
catch(...) {
return Semver();
}
}
//BBS: add a function to load the key-values from xxx.json
@@ -261,18 +264,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
}
};
// The four variant sets are immutable after static init and probed for every
// key of every preset loaded; one merged map makes that a single lookup.
// emplace keeps the first insertion, preserving the first-set-wins priority
// of the else-if chain this replaces.
static const std::unordered_map<std::string, int> variant_class = [] {
std::unordered_map<std::string, int> m;
for (const std::string& k : print_options_with_variant) m.emplace(k, 0);
for (const std::string& k : filament_options_with_variant) m.emplace(k, 1);
for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2);
for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3);
return m;
}();
for(auto& key :config.keys()){
if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){
replace_nil_and_resize(key, process_variant_length);
}
else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){
replace_nil_and_resize(key, filament_variant_length);
}
else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){
replace_nil_and_resize(key, machine_variant_length);
}
else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){
replace_nil_and_resize(key, machine_variant_length * 2);
auto iter = variant_class.find(key);
if (iter == variant_class.end())
continue;
switch (iter->second) {
case 0: replace_nil_and_resize(key, process_variant_length); break;
case 1: replace_nil_and_resize(key, filament_variant_length); break;
case 2: replace_nil_and_resize(key, machine_variant_length); break;
case 3: replace_nil_and_resize(key, machine_variant_length * 2); break;
}
}
}
+26 -3
View File
@@ -148,6 +148,10 @@ public:
PrinterVariant() {}
PrinterVariant(const std::string &name) : name(name) {}
std::string name;
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar) { ar(name); } // PrinterVariant
};
struct PrinterModel {
@@ -156,7 +160,7 @@ public:
std::string name;
//BBS: this is internal id for the printer. Currently only used for searching in database
std::string model_id;
PrinterTechnology technology;
PrinterTechnology technology = ptFFF;
std::string family;
std::vector<PrinterVariant> variants;
std::vector<std::string> default_materials;
@@ -179,6 +183,17 @@ public:
}
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar) // PrinterModel
{
ar(id, name, model_id, technology, family, variants, default_materials,
not_support_bed_types, bed_model, bed_texture, image_bed_type,
bottom_texture_end_name, use_double_extruder_default_texture,
bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect,
hotend_model);
}
};
std::vector<PrinterModel> models;
@@ -190,6 +205,14 @@ public:
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar) // VendorProfile
{
ar(name, id, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials);
}
// Load VendorProfile from an ini file.
// If `load_all` is false, only the header with basic info (name, version, URLs) is loaded.
static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true);
@@ -444,10 +467,10 @@ public:
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
protected:
Preset() = default;
friend class PresetCollection;
friend class PresetBundle;
Preset() = default;
};
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);
File diff suppressed because it is too large Load Diff
+70 -3
View File
@@ -2,10 +2,12 @@
#define slic3r_PresetBundle_hpp_
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "AppConfig.hpp"
#include "enum_bitmask.hpp"
#include <memory>
#include <set>
#include <shared_mutex>
#include <unordered_map>
#include <optional>
@@ -170,6 +172,31 @@ struct PresetBundleMetadata
class PresetBundle
{
public:
// ---- Per-vendor preset cache --------------------------------------------
// One cache file per vendor (plus the Orca filament library), stamped with
// the vendor's own profile version rather than a directory scan. The bytes
// on disk are VendorCacheFile's business (PresetCacheFormat.hpp); what
// lives here is how a cache's contents install into a bundle.
// The cache is not something a caller loads from: a vendor is loaded with
// load_vendor_configs_from_json, which comes from the cache whenever one covers
// it. What is public here is what the cache's own tests drive directly.
// Load a per-vendor cache into this bundle by installing its entries, with
// base_bundle's filament library as the inheritance base. Rejects (returns
// false, with this bundle left clean) unless VendorCacheFile::load accepts
// the file — see its contract for the version and identity checks — and
// every entry installs. Options this build no longer defines are dropped,
// not fatal — the payload names its own keys.
bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr);
// Enable writing a per-vendor cache after a JSON parse (off by default). Cache
// content is pure parse output, so the guard is policy, not correctness: only
// the deliberate generators (load_system_presets_from_json, the cache build
// tool) write files, not every incidental load a dialog performs.
void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; }
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset,
const DynamicPrintConfig &project_config,
@@ -444,8 +471,12 @@ public:
/*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle(
const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
// not the profile JSONs are still there. A whole-vendor load comes from the
// vendor's preset cache whenever one covers the profile on disk, and is parsed
// from the JSONs in `dir` only when none does. Nothing here reads resources.
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
// Export a config bundle file containing all the presets and the names of the active presets.
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
@@ -517,11 +548,49 @@ public:
// Orca: for validation only.
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
// Errors the last load recorded. What the cache's error accounting promises —
// a cache-served vendor reports what its parse would — is pinned against this.
int error_count() const { return m_errors; }
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
std::vector<std::string> merge_presets(PresetBundle &&other);
private:
// Load one vendor from the preset cache installed in `dir`, judged against
// the vendor profile there. False, with this bundle left clean, when there
// is no usable cache and the vendor has to be parsed. This is how
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Load one source-form preset entry into this bundle: resolve `inherits`,
// flatten, validate and register the preset. Returns the reason loading
// failed, empty on success. See the definition for the sharing contract
// between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into
// config_maps (a full config copy each). The cache load passes the names its
// entries inherit — the only ones ever looked up again; the JSON parse
// retains all, not knowing what later subfiles inherit.
std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
// Whether to (re)write a per-vendor cache after a JSON parse.
bool m_generate_vendor_caches { false };
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const;
@@ -529,8 +598,6 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> merge_presets(PresetBundle &&other);
// Update the multicolor information for filaments.
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
+588
View File
@@ -0,0 +1,588 @@
#include "libslic3r/PresetCacheFormat.hpp"
#include <algorithm>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <utility>
#include <boost/crc.hpp>
#include <boost/filesystem.hpp>
#include <boost/iostreams/device/array.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/set.hpp>
#include "libslic3r/Utils.hpp"
namespace Slic3r {
CacheDictionary::CacheDictionary()
{
// ENUM_UNNAMED is index 0 and always the empty name.
m_enum_values.emplace_back();
}
// The ints an enum option holds — one for a coEnum, the whole vector for coEnums.
static std::vector<int> enum_ints(const ConfigOptionDef& def, const ConfigOption* opt)
{
if (def.type == coEnum)
return { opt->getInt() };
return static_cast<const ConfigOptionInts*>(opt)->values;
}
// The name this build gives one of those ints, empty where it has none — a
// nullable option's nil, or a definition carrying no enum_keys_map. Enums are
// written by name so a build that reorders an enum's values still reads it right.
static std::string enum_name_of(const ConfigOptionDef& def, int value)
{
if (def.enum_keys_map != nullptr)
for (const auto& kvp : *def.enum_keys_map)
if (kvp.second == value)
return kvp.first;
return {};
}
void CacheDictionary::collect(const DynamicPrintConfig& config)
{
for (auto it = config.cbegin(); it != config.cend(); ++ it) {
const ConfigOptionDef* def = print_config_def.get(it->first);
if (def == nullptr)
continue; // save_config does not write it either
if (m_key_index.try_emplace(it->first, uint16_t(m_keys.size())).second) {
m_keys.push_back(it->first);
m_types.push_back(uint16_t(def->type));
}
if (def->type != coEnum && def->type != coEnums)
continue;
for (int value : enum_ints(*def, it->second.get())) {
std::string name = enum_name_of(*def, value);
if (! name.empty() && m_enum_index.try_emplace(name, uint16_t(m_enum_values.size())).second)
m_enum_values.push_back(std::move(name));
}
}
}
uint16_t CacheDictionary::key_index(const t_config_option_key& key) const
{
auto it = m_key_index.find(key);
if (it == m_key_index.end())
throw std::runtime_error("preset cache: option " + key + " was never collected into the dictionary");
return it->second;
}
uint16_t CacheDictionary::enum_index(const std::string& name) const
{
if (name.empty())
return ENUM_UNNAMED;
auto it = m_enum_index.find(name);
return it == m_enum_index.end() ? ENUM_UNNAMED : it->second;
}
void CacheDictionary::save(cereal::BinaryOutputArchive& ar) const
{
// Checked here rather than left to the caller: an index that wrapped would
// be written silently, and nothing downstream could tell.
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
throw std::runtime_error("preset cache: the option dictionary outgrew the uint16 it is indexed with");
ar(m_keys, m_types, m_enum_values);
}
void CacheDictionary::load(cereal::BinaryInputArchive& ar)
{
ar(m_keys, m_types, m_enum_values);
if (m_keys.size() != m_types.size())
throw std::runtime_error("preset cache: dictionary key and type tables differ in length");
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
throw std::runtime_error("preset cache: dictionary is larger than the uint16 it is indexed with");
if (m_enum_values.empty() || ! m_enum_values.front().empty())
throw std::runtime_error("preset cache: dictionary is missing its unnamed-enum slot");
// Resolved once per file: every option read after this is a vector index.
m_defs.resize(m_keys.size());
for (size_t i = 0; i < m_keys.size(); ++ i) {
const ConfigOptionDef* def = print_config_def.get(m_keys[i]);
m_defs[i] = (def != nullptr && uint16_t(def->type) == m_types[i]) ? def : nullptr;
}
}
// ---- one config -----------------------------------------------------------
static void save_enum_option(cereal::BinaryOutputArchive& ar, const ConfigOptionDef& def,
const ConfigOption* opt, const CacheDictionary& dict)
{
const std::vector<int> values = enum_ints(def, opt);
ar(uint32_t(values.size()));
for (int value : values) {
const uint16_t idx = dict.enum_index(enum_name_of(def, value));
ar(idx);
if (idx == CacheDictionary::ENUM_UNNAMED)
ar(int32_t(value));
}
}
// `config` may be null, in which case the option is read and dropped.
static void load_enum_option(cereal::BinaryInputArchive& ar, ConfigOptionType type,
const ConfigOptionDef* def, DynamicPrintConfig* config,
const CacheDictionary& dict)
{
uint32_t cnt = 0;
ar(cnt);
if (type == coEnum && cnt != 1)
throw std::runtime_error("preset cache: a scalar enum carrying more than one value");
// Every element is read whatever happens, so the stream stays in sync and
// whatever follows this option still loads.
bool usable = def != nullptr && config != nullptr;
std::vector<int> values;
values.reserve(cnt);
for (uint32_t i = 0; i < cnt; ++ i) {
uint16_t idx = 0;
ar(idx);
if (! dict.valid_enum_index(idx))
throw std::runtime_error("preset cache: enum value index past the end of the dictionary");
if (idx == CacheDictionary::ENUM_UNNAMED) {
// An int the writer could not name — a nil, or an option whose
// definition carried no enum_keys_map. It travels verbatim.
int32_t raw = 0;
ar(raw);
values.push_back(int(raw));
continue;
}
if (! usable)
continue; // the index above was this element's whole payload
if (def->enum_keys_map == nullptr) {
usable = false; // this build no longer maps this option's names
continue;
}
const auto it = def->enum_keys_map->find(dict.enum_name_at(idx));
if (it == def->enum_keys_map->end()) {
usable = false; // a value this build dropped: the option goes with it
continue;
}
values.push_back(it->second);
}
if (! usable)
return;
if (type == coEnum) {
config->set_key_value(def->opt_key, new ConfigOptionEnumGeneric(def->enum_keys_map, values.front()));
} else {
auto* opt = def->nullable ? static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGenericNullable(def->enum_keys_map))
: static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGeneric(def->enum_keys_map));
opt->values = std::move(values);
config->set_key_value(def->opt_key, opt);
}
}
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict)
{
struct Written { uint16_t idx; const ConfigOptionDef* def; const ConfigOption* opt; };
std::vector<Written> written;
written.reserve(config.size());
for (auto it = config.cbegin(); it != config.cend(); ++ it)
if (const ConfigOptionDef* def = print_config_def.get(it->first))
written.push_back({ dict.key_index(it->first), def, it->second.get() });
ar(uint32_t(written.size()));
for (const Written& w : written) {
ar(w.idx);
if (w.def->type == coEnum || w.def->type == coEnums)
save_enum_option(ar, *w.def, w.opt, dict);
else
w.def->save_option_to_archive(ar, w.opt);
}
}
// `config` null means: read everything, keep nothing.
static void read_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig* config, const CacheDictionary& dict)
{
uint32_t cnt = 0;
ar(cnt);
if (config != nullptr)
config->clear();
// Reused across the loop: constructing a ConfigOptionDef per dropped option
// would allocate its strings and vectors for nothing.
ConfigOptionDef scratch;
for (uint32_t i = 0; i < cnt; ++ i) {
uint16_t idx = 0;
ar(idx);
if (! dict.valid_key_index(idx))
throw std::runtime_error("preset cache: option index past the end of the dictionary");
const ConfigOptionType type = dict.type_at(idx);
const ConfigOptionDef* def = dict.def_at(idx);
if (type == coEnum || type == coEnums) {
load_enum_option(ar, type, def, config, dict);
} else if (def != nullptr && config != nullptr) {
config->set_key_value(def->opt_key, def->load_option_from_archive(ar));
} else {
// Read by the type the writer recorded, then drop: the same outcome
// a JSON profile gets for an option this build no longer has.
scratch.type = type;
std::unique_ptr<ConfigOption> discard(scratch.load_option_from_archive(ar));
}
}
}
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict)
{
read_config(ar, &config, dict);
}
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict)
{
read_config(ar, nullptr, dict);
}
// ---- The per-vendor cache file (<vendor>.opc) -----------------------------
namespace {
#pragma pack(push, 1)
struct CacheFileHeader {
uint32_t magic;
uint32_t version;
uint64_t data_size;
uint32_t crc32;
};
#pragma pack(pop)
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
// Bump when the wire format changes in a way the payload cannot describe
// itself out of: reordering, removing or retyping a field of a hand-written
// serialize() (VendorProfile and its nested types, CachedPreset via
// save_entries below), or a change to the cache's own layout or the
// meaning of its stamps. Option-schema drift is NOT such a change — the
// dictionary handles it, which is why this no longer moves every release.
constexpr uint32_t CACHE_VERSION = 1;
// A stamp-string read that refuses an absurd length before allocating anything.
// The stamps are read from files named from the outside (peek_version is
// pointed at whatever <vendor>.opc a directory holds), so the length word may
// be arbitrary bytes — and a resize to a garbage 64-bit length does not fail as
// a catchable bad_alloc here, it takes the app down through the out-of-memory
// handler. A vendor name or profile version is a short token; anything longer
// is not a cache this build wrote.
std::string read_bounded_string(cereal::BinaryInputArchive& ar)
{
constexpr uint64_t MAX_STAMP_LEN = 1024;
cereal::size_type len = 0;
ar(cereal::make_size_tag(len));
if (uint64_t(len) > MAX_STAMP_LEN)
throw std::runtime_error("preset cache: string length out of bounds");
std::string s(size_t(len), '\0');
ar(cereal::binary_data(s.data(), size_t(len)));
return s;
}
// The prologue every cache reader starts with: the format version, then the
// vendor's identity. Returns the vendor version stamped on a body this build can
// read, empty on anything else — which is the same answer as "not this vendor".
std::string read_cache_stamps(cereal::BinaryInputArchive& ar, const std::string& expected_vendor_name)
{
// The version is judged before anything variable-length is read: on a body
// that is not a per-vendor cache of this version, the bytes where a string
// length would sit may be arbitrary framing.
uint32_t cache_version = 0;
ar(cache_version);
if (cache_version != CACHE_VERSION)
return {};
const std::string vendor_name = read_bounded_string(ar);
const std::string vendor_version = read_bounded_string(ar);
if (vendor_name != expected_vendor_name)
return {};
return vendor_version;
}
// A cache stays usable as long as it was built from a vendor profile at least
// as new as the one now on disk. Profiles whose version is invalid cannot be
// judged this way and are never served from cache; where no profile sits
// beside the cache at all, nothing can be newer than it — that state is passed
// as Semver::inf(), which no real profile can carry (an invalid version could
// not say it apart from "profile there but unjudgeable", and zero would
// collide with a genuine "0.0.0"). This is the serve rule; the install rule
// (cache_covers in PresetBundle.cpp) deliberately reads an unjudgeable profile
// the other way, so the two are not one function.
bool cache_covers_version(const std::string& cached, const Semver& on_disk)
{
if (on_disk == Semver::inf())
return true; // before parsing `cached`: nothing exists that the stamp must cover
if (! on_disk.valid())
return false;
const auto cached_ver = Semver::parse(cached);
return cached_ver && *cached_ver >= on_disk;
}
// CachedPreset on the wire: all fields, declaration order, in one place.
// `config` writes, reads or skips the config sitting in the middle of that
// order — the three things a reader can want to do with it — so save, load and
// the name peek below cannot drift apart. Keep in sync with the struct in
// PresetCacheFormat.hpp and bump CACHE_VERSION on change. Written here rather
// than as a serialize() member because the config needs the file's dictionary,
// which cereal cannot thread through one.
template<class Archive, class Entry, class ConfigFn>
void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
{
ar(e.name, e.sub_path);
config();
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
}
// The count comes from a file that has already passed magic and CRC, but a
// reserve is a promise to allocate: cap it and let push_back grow the rest.
constexpr uint32_t MAX_RESERVED_ENTRIES = 4096;
void save_entries(cereal::BinaryOutputArchive& ar,
const std::vector<CachedPreset>& entries,
const CacheDictionary& dict)
{
ar(uint32_t(entries.size()));
for (const CachedPreset& e : entries)
visit_entry(ar, e, [&] { save_config(ar, e.config_src, dict); });
}
void load_entries(cereal::BinaryInputArchive& ar,
std::vector<CachedPreset>& entries,
const CacheDictionary& dict)
{
uint32_t cnt = 0;
ar(cnt);
entries.clear();
entries.reserve(std::min(cnt, MAX_RESERVED_ENTRIES));
for (uint32_t i = 0; i < cnt; ++ i) {
CachedPreset e;
visit_entry(ar, e, [&] { load_config(ar, e.config_src, dict); });
entries.push_back(std::move(e));
}
}
// Read a raw cache body: verify magic, size, CRC.
bool read_cache_blob(const std::string& path, std::string& out_blob)
{
try {
boost::nowide::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open())
return false;
CacheFileHeader fhdr;
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
return false;
if (fhdr.magic != CACHE_MAGIC)
return false;
// data_size is 8 bytes from a file nothing has authenticated yet, and
// it is about to size an allocation. The body is the whole of the file
// behind the header — anything else is not a cache this build wrote.
ifs.seekg(0, std::ios::end);
const std::streamoff file_size = ifs.tellg();
if (file_size < std::streamoff(sizeof(fhdr)) ||
fhdr.data_size == 0 ||
fhdr.data_size != uint64_t(file_size) - sizeof(fhdr))
return false;
ifs.seekg(sizeof(fhdr), std::ios::beg);
out_blob.assign(fhdr.data_size, '\0');
if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size)))
return false;
boost::crc_32_type crc;
crc.process_bytes(out_blob.data(), out_blob.size());
if (crc.checksum() != fhdr.crc32) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: CRC mismatch: " << path;
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: read failed (" << path << "): " << e.what();
return false;
}
}
// Write a cache body behind the standard 20-byte file header. False when the
// file could not be opened or written whole.
bool write_cache_blob(const std::string& path, const std::string& blob)
{
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
// Written beside the target and moved into place, as AppConfig::save does:
// a cache is truncated and rewritten in full, so a write that dies partway
// would otherwise leave a header claiming more body than the file holds.
// The PID suffix also keeps two instances writing the same vendor from
// interleaving.
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
try {
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
return false;
}
CacheFileHeader fhdr;
fhdr.magic = CACHE_MAGIC;
fhdr.version = CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
ofs.close(); // flush; close() raises failbit on error
if (! ofs.good()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
if (const std::error_code ec = rename_file(tmp_path, path)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
} // anonymous namespace
// static
bool VendorCacheFile::save(const std::string& path, const std::string& vendor_name,
const std::string& vendor_version, const VendorCacheData& data)
{
try {
// Collected before anything is written: the dictionary sits ahead of the
// entries so a reader resolves it once and then indexes.
CacheDictionary dict;
for (const std::vector<CachedPreset>* entries : { &data.process_entries, &data.filament_entries, &data.machine_entries })
for (const CachedPreset& e : *entries)
dict.collect(e.config_src);
std::ostringstream body(std::ios::binary);
{
cereal::BinaryOutputArchive ar(body);
ar(CACHE_VERSION);
ar(vendor_name, vendor_version);
dict.save(ar);
ar(data.vendors);
save_entries(ar, data.process_entries, dict);
save_entries(ar, data.filament_entries, dict);
save_entries(ar, data.machine_entries, dict);
ar(data.parse_errors);
}
return write_cache_blob(path, body.str());
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: failed to save vendor cache " << path << ": " << e.what();
return false;
}
}
// static
bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorCacheData& data)
{
std::string blob;
if (! read_cache_blob(path, blob))
return false;
try {
// Read in place: an istringstream would copy the blob once more just to
// stream over it.
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
cereal::BinaryInputArchive ar(body);
const std::string vendor_version = read_cache_stamps(ar, expected_vendor_name);
if (vendor_version.empty() || ! cache_covers_version(vendor_version, expected_vendor_version))
return false;
CacheDictionary dict;
dict.load(ar);
ar(data.vendors);
load_entries(ar, data.process_entries, dict);
load_entries(ar, data.filament_entries, dict);
load_entries(ar, data.machine_entries, dict);
ar(data.parse_errors);
if (data.vendors.find(expected_vendor_name) == data.vendors.end())
throw std::runtime_error("vendor cache does not carry its own vendor profile");
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: rejecting vendor cache " << path << ": " << e.what();
return false;
}
}
// static
std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name)
{
try {
boost::nowide::ifstream ifs(path, std::ios::binary);
CacheFileHeader fhdr;
if (! ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)) || fhdr.magic != CACHE_MAGIC)
return {};
// Only the head of the body is read, and its CRC left unverified: the
// stamps sit at the front, and this answers "what version is this?"
// without paying for tens of megabytes. Callers that need to know the
// file is whole use usable_version instead.
std::string head(static_cast<size_t>(std::min<uint64_t>(fhdr.data_size, 1024)), '\0');
if (! ifs.read(&head[0], static_cast<std::streamsize>(head.size())))
return {};
std::istringstream body(head, std::ios::binary);
cereal::BinaryInputArchive ar(body);
return read_cache_stamps(ar, expected_vendor_name);
} catch (const std::exception&) {
return {};
}
}
// static
Semver VendorCacheFile::usable_version(const std::string& path, const std::string& expected_vendor_name)
{
std::string blob;
if (! read_cache_blob(path, blob))
return Semver::invalid();
try {
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
cereal::BinaryInputArchive ar(body);
const auto ver = Semver::parse(read_cache_stamps(ar, expected_vendor_name));
return ver ? *ver : Semver::invalid();
} catch (const std::exception&) {
return Semver::invalid();
}
}
// static
bool VendorCacheFile::carries_preset(const std::string& path, const std::string& vendor_name,
Preset::Type type, const std::string& preset_name)
{
std::string blob;
if (! read_cache_blob(path, blob))
return false;
try {
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
cereal::BinaryInputArchive ar(body);
if (read_cache_stamps(ar, vendor_name).empty())
return false;
CacheDictionary dict;
dict.load(ar);
VendorMap vendors;
ar(vendors);
// Reused: every entry overwrites it, and only its name is ever looked at.
CachedPreset entry;
// Written in this order by save. The list that could carry the preset
// is the last one worth reading.
for (Preset::Type kind : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER }) {
uint32_t cnt = 0;
ar(cnt);
for (uint32_t i = 0; i < cnt; ++ i) {
visit_entry(ar, entry, [&] { skip_config(ar, dict); });
if (kind == type && entry.name == preset_name)
return true;
}
if (kind == type)
return false;
}
return false;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not read preset names from " << path << ": " << e.what();
return false;
}
}
} // namespace Slic3r
+192
View File
@@ -0,0 +1,192 @@
#ifndef slic3r_PresetCacheFormat_hpp_
#define slic3r_PresetCacheFormat_hpp_
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
#include <cereal/archives/binary.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Semver.hpp"
namespace Slic3r {
// How the preset cache writes a DynamicPrintConfig.
//
// Not through the global cereal hooks in PrintConfig.hpp: those key an option by
// its serialization_key_ordinal, which ConfigDef::add assigns by declaration
// order at static-init time. Inserting one option into the middle of
// PrintConfig.cpp shifts every later ordinal, and the lookup on the way back in
// then SUCCEEDS on the wrong option — where the two share a type, and hundreds
// of coFloat/coBool/coInt options do, the bytes deserialize cleanly into the
// wrong key. Silently wrong print settings, no error. Those hooks are also the
// undo/redo wire format, where the process cannot change underneath them, so
// they stay as they are and the cache keys by name instead.
//
// Names are not repeated per preset. Each cache file carries one dictionary of
// the distinct opt_keys it uses, the type each was written as, and the distinct
// enum value names; an option on the wire is then a uint16 index into it plus
// its value. The dictionary is resolved to this build's option definitions once
// per file, after which reading an option is a vector index.
class CacheDictionary
{
public:
CacheDictionary();
// Index reserved in the enum table for an int the writing build could not
// name — a nullable option's nil, or a definition carrying no
// enum_keys_map. The raw int32 follows it on the wire and is loaded
// verbatim, so those values survive too.
static constexpr uint16_t ENUM_UNNAMED = 0;
// ---- writing ----
// Record every key and enum value `config` uses. Call for every config that
// will be written, before writing the dictionary.
void collect(const DynamicPrintConfig& config);
uint16_t key_index(const t_config_option_key& key) const;
// ENUM_UNNAMED for an empty name or one that was never collected.
uint16_t enum_index(const std::string& name) const;
// ---- reading ----
// The definition an index resolves to in THIS build, or nullptr where the
// key is unknown here or is now defined with a different type. A nullptr
// entry's value is still read — using type_at(idx), the type the writer
// recorded — and then dropped, which is what a JSON profile gets for an
// option this build no longer has.
const ConfigOptionDef* def_at(uint16_t idx) const { return m_defs[idx]; }
ConfigOptionType type_at(uint16_t idx) const { return ConfigOptionType(m_types[idx]); }
const std::string& enum_name_at(uint16_t idx) const { return m_enum_values[idx]; }
// m_defs, not m_keys: only load() sizes it, so this is false for every index
// on a dictionary that was collected rather than read.
bool valid_key_index(uint16_t idx) const { return size_t(idx) < m_defs.size(); }
bool valid_enum_index(uint16_t idx) const { return size_t(idx) < m_enum_values.size(); }
// The layout these two agree on is covered by CACHE_VERSION (PresetCacheFormat.cpp);
// bump it when they change.
// Throws when either table outgrew the uint16 the wire format indexes it
// with. Both are bounded by the option count (912 at the time of writing), so
// that is a build-time failure in CI, not a runtime one.
void save(cereal::BinaryOutputArchive& ar) const;
// Throws on a dictionary that cannot be indexed as written.
void load(cereal::BinaryInputArchive& ar);
private:
// Indices are uint16, so a table may hold at most this many entries.
static constexpr size_t MAX_ENTRIES = 0xFFFF;
std::vector<std::string> m_keys;
// ConfigOptionType, as written. Sixteen bits, not eight: coVectorType is
// 0x4000, so every vector type — coFloats, coEnums, coStrings — is above
// 255, and a byte would fold each one onto its scalar counterpart.
std::vector<uint16_t> m_types;
std::vector<std::string> m_enum_values; // [ENUM_UNNAMED] is always empty
// Writing.
std::unordered_map<std::string, uint16_t> m_key_index;
std::unordered_map<std::string, uint16_t> m_enum_index;
// Reading, resolved once by load().
std::vector<const ConfigOptionDef*> m_defs;
};
// One config, keyed through `dict`. Options print_config_def does not know are
// not written: nothing could give them a type on the way back in.
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict);
// Throws only on a payload that cannot be indexed; an option this build cannot
// place is dropped, not fatal.
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict);
// Consume one config without building it, for a reader that only wants what
// comes after.
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
// One preset as its JSON subfile states it: the config diff, the name of the
// preset it inherits, and the parse metadata — everything the parse phase of
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
// is resolved when the entry is installed, against whatever filament library
// is loaded then, so a cache carries no other vendor's values and no other
// vendor's update can make it stale.
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
// field below in this order — once, for the save, the load and the name peek alike.
struct CachedPreset
{
std::string name;
std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits;
std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id;
std::string filament_id;
std::vector<std::string> renamed_from;
};
// What one per-vendor cache file carries besides its stamps: the vendor profile
// map, the presets in source form, and how many errors their parse counted.
struct VendorCacheData
{
VendorMap vendors;
std::vector<CachedPreset> process_entries;
std::vector<CachedPreset> filament_entries;
std::vector<CachedPreset> machine_entries;
uint64_t parse_errors = 0;
};
// A per-vendor preset cache file (<vendor>.opc): a 20-byte header (magic, format
// version, body size, CRC) framing one cereal body — stamps (format version,
// vendor name, vendor profile version), the option dictionary, then the
// VendorCacheData. Everything about those bytes lives here; when a vendor is
// served from its cache, and how entries install into a bundle, is
// PresetBundle's business.
class VendorCacheFile
{
public:
// Save one vendor (vendor_name at vendor_version). False when the file
// could not be written whole.
static bool save(const std::string& path, const std::string& vendor_name,
const std::string& vendor_version, const VendorCacheData& data);
// Read a whole cache into `data`. False — with `data` in an unspecified
// state — unless the file is a cache this build wrote, its CRC holds, it
// names this vendor, it was built from a vendor profile at least as new as
// `expected_vendor_version`, and it carries its own vendor profile. An
// invalid expected version (a profile whose version
// cannot be judged) is never served from cache; Semver::inf() (no profile
// beside the cache at all) accepts whatever is cached.
static bool load(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorCacheData& data);
// Read the profile version a cache was stamped with, without deserializing
// its presets. Empty if the file is unreadable, not a cache this build
// understands, or not this vendor's. This is how an installed vendor's
// version is known when only its cache is installed.
static std::string peek_version(const std::string& path, const std::string& expected_vendor_name);
// The profile version an installed cache can actually be served at, or an
// invalid Semver when the file is not a cache this build can read. Unlike
// peek_version this verifies the body's CRC, at the cost of reading the
// whole file: where the cache is the vendor's whole installation, "a file
// is there" is not enough to call it installed, and a vendor wrongly
// believed installed is never repaired.
static Semver usable_version(const std::string& path, const std::string& expected_vendor_name);
// Whether a cache carries a preset of `type` under `preset_name`, without
// installing any of them. False when the file is not a cache this build can
// read. The three kinds are written in one stream, so reaching the machines
// means reading past the processes and filaments — their configs are consumed
// and dropped rather than built. This is how a build that ships caches instead
// of preset JSONs answers "which vendor carries this preset?".
static bool carries_preset(const std::string& path, const std::string& vendor_name,
Preset::Type type, const std::string& preset_name);
};
} // namespace Slic3r
#endif // slic3r_PresetCacheFormat_hpp_
+1 -1
View File
@@ -5827,7 +5827,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
Polygon PrintInstance::get_convex_hull_2d() {
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
poly.douglas_peucker(0.1);
poly.douglas_peucker(scale_(0.1));
return poly;
}
+13 -4
View File
@@ -3469,9 +3469,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("sparse_infill_smooth_factor", coPercent);
def->label = L("Sparse infill smooth factor");
def->category = L("Strength");
def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, "
"while 100% produces the largest possible curves between adjacent infill lines. "
"Currently applies only to the Hilbert Curve.");
def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, "
"while 100% produces the largest possible curves between adjacent infill lines.");
def->sidetext = "%";
def->min = 0;
def->max = 100;
@@ -10426,6 +10425,16 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
}
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
const std::vector<int> &variant_index, int stride)
{
// A single-value object or region override applies to every nozzle variant.
std::vector<int> indices = variant_index;
if (source.size() == 1 && !source.is_nil(0))
std::fill(indices.begin(), indices.end(), 0);
target.set_to_index(&source, indices, stride);
}
//used for object/region config
//use the smallest of multiple to single
@@ -11503,7 +11512,7 @@ void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPr
else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_dest);
opt_vec_src->set_to_index(opt_vec_dest, variant_index, stride);
set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index, stride);
}
}
}
+77 -1
View File
@@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
}
}
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
// than one line per infill wall; a single line makes them plain crossing lines with nothing to round.
inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1)
{
switch (pattern) {
case ipHilbertCurve:
case ipOctagramSpiral:
case ipLightning:
case ipHoneycomb:
case ip3DHoneycomb:
case ipConcentric:
case ipCrossHatch:
return true;
case ipGrid:
case ipTriangles:
case ipStars:
return multiline > 1;
default:
return false;
}
}
enum class IroningType {
NoIroning,
TopSurfaces,
@@ -842,6 +865,9 @@ extern std::set<std::string> printer_options_with_variant_1;
extern std::set<std::string> printer_options_with_variant_2;
extern std::set<std::string> empty_options;
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
const std::vector<int> &variant_index, int stride = 1);
extern std::set<std::string> filament_dev_options;
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
@@ -2394,6 +2420,55 @@ static void set_flush_volumes_matrix(std::vector<T> &out_matrix, const std::vect
}
}
template<class T>
static bool has_zero_flush_volume_for_used_filaments(const std::vector<T> &fv_matrix,
const std::vector<T> &flush_multipliers,
const std::vector<int> &used_filaments)
{
if (used_filaments.size() < 2 || flush_multipliers.empty())
return false;
if (fv_matrix.size() % flush_multipliers.size() != 0)
return false;
const size_t matrix_len = fv_matrix.size() / flush_multipliers.size();
const size_t row_len = size_t(std::sqrt(double(matrix_len)));
if (row_len < 2 || row_len * row_len != matrix_len)
return false;
std::vector<int> filtered_filaments;
filtered_filaments.reserve(used_filaments.size());
for (int filament_id : used_filaments) {
if (filament_id <= 0 || filament_id > int(row_len))
continue;
if (std::find(filtered_filaments.begin(), filtered_filaments.end(), filament_id) == filtered_filaments.end())
filtered_filaments.push_back(filament_id);
}
if (filtered_filaments.size() < 2)
return false;
for (T multiplier : flush_multipliers) {
if (multiplier == 0)
return true;
}
for (size_t nozzle_idx = 0; nozzle_idx < flush_multipliers.size(); nozzle_idx++) {
const size_t block_offset = nozzle_idx * matrix_len;
for (int from_id : filtered_filaments) {
for (int to_id : filtered_filaments) {
if (from_id == to_id)
continue;
const size_t matrix_idx = block_offset + size_t(from_id - 1) * row_len + size_t(to_id - 1);
if (matrix_idx < fv_matrix.size() && fv_matrix[matrix_idx] == 0)
return true;
}
}
}
return false;
}
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id);
} // namespace Slic3r
@@ -2413,7 +2488,8 @@ namespace cereal {
archive(serialization_key_ordinal);
assert(serialization_key_ordinal > 0);
auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal);
assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end());
if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end())
throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale");
config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive));
}
}
+1 -1
View File
@@ -3812,7 +3812,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
else {
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(my_opt);
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(it->second.get());
opt_vec_src->set_to_index(opt_vec_dest, variant_index, 1);
set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index);
}
}
}
+13
View File
@@ -190,6 +190,19 @@ public:
os << self.to_string();
return os;
}
// cereal: round-trip through the standard 3-part string (major.minor.patch).
// to_string() uses a BBS 4-part format that semver_parse() cannot read back.
template<class Archive>
std::string save_minimal(const Archive&) const { return to_string_sf(); }
template<class Archive>
void load_minimal(const Archive&, const std::string& s) {
auto v = Semver::parse(s);
if (! v)
throw std::runtime_error("Semver: cannot parse serialized version: " + s);
*this = std::move(*v);
}
private:
semver_t ver;
+38 -5
View File
@@ -3,6 +3,7 @@
#include <iomanip>
#include <locale>
#include <set>
#include <utility>
#include <functional>
#include <type_traits>
@@ -18,6 +19,7 @@
#include <openssl/md5.h>
#include "libslic3r.h"
#include "Semver.hpp"
//define CLI errors
@@ -722,11 +724,42 @@ void copy_directory_recursively(const boost::filesystem::path& source,
std::function<bool(const std::string)> filter = nullptr,
bool merge_mode = false);
// Install vendor bundles from resources directory to data directory
// bundle_names: vector of vendor bundle names (without .json extension)
// resource_subdir: subdirectory under resources_dir() (default: "profiles")
// data_subdir: subdirectory under data_dir() (default: "system")
// Returns: true if all bundles installed successfully, false otherwise
// ---- Vendor installation on disk ------------------------------------------
// How a vendor bundle is installed from resources into data_dir()/system: as
// its profile and preset JSONs or, in a build that ships preset caches, as its
// .opc preset cache alone. Loading what is installed is PresetBundle's business;
// the cache file format itself is VendorCacheFile's (PresetCacheFormat.hpp).
// True if `vendor` is installed in data_dir()/system. A build that ships preset
// caches installs the cache alone, so it — not the profile — marks a vendor
// installed; a cache this build cannot read marks nothing.
bool is_vendor_installed(const std::string& vendor);
// The version the installed vendor would be loaded at: its cache's stamp while
// that covers the profile beside it, the profile's own version once it does not.
// Invalid Semver if neither form is installed.
Semver installed_vendor_version(const std::string& vendor);
// Remove every form `vendor` can be installed as from data_dir()/system: its
// profile, its preset cache, and its preset directory.
void remove_installed_vendor(const std::string& vendor);
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that
// ships preset caches instead of the raw profile JSONs, by its cache alone.
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);
// The version a build ships `vendor` at: whichever of its preset cache and its
// profile is newer, that being the one installing lays down. Invalid Semver if the
// build ships neither.
Semver resource_vendor_version(const std::string& vendor);
// Install vendors from the resources directory into the data directory, each as
// its preset cache or as its profile and preset JSONs — whichever of the two the
// build ships at the newer version. Anything the previous install of that vendor
// left behind goes, so only the form just installed is there to be loaded.
// bundle_names: vendor names, without extension.
// Every bundle that can be installed is, whatever the others do. Returns false
// if any named bundle could not be installed.
bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
const std::string& resource_subdir = "profiles",
const std::string& data_subdir = "system");
+141 -13
View File
@@ -17,6 +17,10 @@
#include "Platform.hpp"
#include "Time.hpp"
#include "libslic3r.h"
// For the vendor-installation helpers: the vendor profile version
// (get_version_from_json) and the preset cache stamp (VendorCacheFile).
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#ifdef __APPLE__
#include "MacUtils.hpp"
@@ -1724,6 +1728,85 @@ void copy_directory_recursively(const boost::filesystem::path& source,
return;
}
// ---- Vendor installation on disk ------------------------------------------
// Whether a cache stamped `cache_ver` still speaks for a vendor whose profile on
// disk claims `profile_ver`: it does unless the profile has moved ahead of it. A
// profile that is missing or carries no judgeable version cannot be ahead of
// anything. The one rule behind both "which form gets installed" and "which form
// is installed"; they must not drift apart. Deliberately NOT the serve rule
// (VendorCacheFile::load), which refuses an unjudgeable profile instead.
static bool cache_covers(const Semver& cache_ver, const Semver& profile_ver)
{
return cache_ver.valid() && (! profile_ver.valid() || cache_ver >= profile_ver);
}
bool is_vendor_installed(const std::string& vendor)
{
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
// A cache is the whole of a cache-only installation, so a file this build
// cannot serve the vendor from is not an installation. Left counted as one,
// the updater would never lay a working copy down.
return boost::filesystem::exists(dir / (vendor + ".json"))
|| VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor).valid();
}
Semver installed_vendor_version(const std::string& vendor)
{
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
const boost::filesystem::path json = dir / (vendor + ".json");
// Guarded: get_version_from_json logs an error and throws-and-catches its way
// to an invalid version on a file that is not there, and a cache-only vendor
// never has one.
const Semver from_json = boost::filesystem::exists(json) ? get_version_from_json(json.string()) : Semver();
const Semver from_cache = VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor);
// Whichever form a load would serve.
return cache_covers(from_cache, from_json) ? from_cache : from_json;
}
void remove_installed_vendor(const std::string& vendor)
{
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
boost::filesystem::remove(dir / (vendor + ".json"));
boost::filesystem::remove(dir / (vendor + ".opc"));
if (boost::filesystem::exists(dir / vendor))
boost::filesystem::remove_all(dir / vendor);
}
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir)
{
std::set<std::string> names;
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
const auto& path = dir_entry.path();
if (Slic3r::is_json_file(path.string()) || path.extension() == ".opc")
names.insert(path.stem().string());
}
return names;
}
// A vendor's preset cache is the whole of its installation: it carries the presets,
// the vendor profile and the version they were built at, so where one ships nothing
// else needs copying. Unless the profile beside it claims a newer version — a cache
// generated before that profile was bumped is out of date, and a cache that cannot
// be read is no installation at all — and the vendor is installed the way it was
// before caches existed, as its profile and the preset JSONs it points at. Returns
// the version the cache is stamped with, invalid when it is not the form to install.
static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor)
{
const auto cache_ver = Semver::parse(VendorCacheFile::peek_version((dir / (vendor + ".opc")).string(), vendor));
if (! cache_ver)
return Semver::invalid();
const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string());
return cache_covers(*cache_ver, profile_ver) ? *cache_ver : Semver::invalid();
}
Semver resource_vendor_version(const std::string& vendor)
{
const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles";
const Semver ver = installable_cache_version(dir, vendor);
return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string());
}
bool install_vendor_bundles_from_resources(
const std::vector<std::string>& bundle_names,
const std::string& resource_subdir,
@@ -1736,37 +1819,82 @@ bool install_vendor_bundles_from_resources(
BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources...";
// One vendor that cannot be installed is one vendor missing, not a reason to
// leave the rest uninstalled. The caller is told, and every bundle that can
// be laid down is.
bool all_installed = true;
for (const auto &bundle : bundle_names) {
try {
if (bundle.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Refusing to install a bundle with no name";
all_installed = false;
continue;
}
// Install the JSON file
auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json");
auto path_in_vendors = (vendor_path / bundle).replace_extension(".json");
auto cache_in_rsrc = (rsrc_path / bundle).replace_extension(".opc");
auto cache_in_vendors = (vendor_path / bundle).replace_extension(".opc");
if (!fs::exists(path_in_rsrc)) {
// Either form of the vendor will do: a build may ship it as a cache alone.
if (!fs::exists(path_in_rsrc) && !fs::exists(cache_in_rsrc)) {
BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle;
return false;
all_installed = false;
continue;
}
// Create target directory if needed
if (!fs::exists(vendor_path))
fs::create_directories(vendor_path);
// Copy JSON file
std::string error_message;
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
if (cfr != CopyFileResult::SUCCESS) {
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
return false;
bool installed_cache = false;
if (installable_cache_version(rsrc_path, bundle).valid()) {
installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS;
if (! installed_cache) {
BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message;
} else if (! VendorCacheFile::usable_version(cache_in_vendors.string(), bundle).valid()) {
// The copy is what will be loaded, so it — not the kilobyte
// peek that chose this form — decides whether the profile
// beside it can go.
BOOST_LOG_TRIVIAL(warning) << "Installed cache for " << bundle << " cannot be read; installing its profile instead";
boost::system::error_code ec;
fs::remove(cache_in_vendors, ec);
installed_cache = false;
}
}
if (! installed_cache) {
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
if (cfr != CopyFileResult::SUCCESS) {
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
all_installed = false;
continue;
}
// Only now: an earlier install's cache would shadow this profile,
// but removing it before the profile lands would leave neither.
boost::system::error_code ec;
fs::remove(cache_in_vendors, ec);
} else {
// Left in place, an earlier install's profile would shadow the cache.
boost::system::error_code ec;
fs::remove(path_in_vendors, ec);
if (ec)
BOOST_LOG_TRIVIAL(warning) << "Could not remove the superseded profile " << path_in_vendors.string() << ": " << ec.message();
}
// Copy the vendor directory (if it exists)
auto dir_in_rsrc = rsrc_path / bundle;
auto dir_in_vendors = vendor_path / bundle;
if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
// Remove existing directory
if (fs::exists(dir_in_vendors))
fs::remove_all(dir_in_vendors);
// Whatever is installed came from an earlier version of this vendor and
// would be parsed in place of the one being installed now.
if (fs::exists(dir_in_vendors))
fs::remove_all(dir_in_vendors);
if (! installed_cache && fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
fs::create_directories(dir_in_vendors);
// Copy with file filter (same as PresetUpdater::install_bundles_rsrc)
@@ -1787,11 +1915,11 @@ bool install_vendor_bundles_from_resources(
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what();
return false;
all_installed = false;
}
}
return true;
return all_installed;
}
void save_string_file(const boost::filesystem::path& p, const std::string& str)
+5
View File
@@ -620,6 +620,8 @@ set(SLIC3R_GUI_SOURCES
plugin/host/PluginHostSlicing.cpp
plugin/host/PluginHostUi.cpp
plugin/host/PluginHostUi.hpp
plugin/host/PluginPages.cpp
plugin/host/PluginPages.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
@@ -640,6 +642,9 @@ set(SLIC3R_GUI_SOURCES
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
plugin/pluginTypes/pages/PagesPluginCapability.hpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
+3 -8
View File
@@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot:
cfg.models_variants_installed.erase(it ++);
else
++ it;
// Read the active config bundle, parse the config version.
PresetBundle bundle;
//BBS: change directoties by design
//bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
for (const auto &vp : bundle.vendors)
if (vp.second.id == cfg.name)
cfg.version.config_version = vp.second.config_version;
// Orca: the version the vendor is installed at, read from its profile or —
// where the cache is the whole installation — from the cache's own stamp.
cfg.version.config_version = installed_vendor_version(cfg.name);
snapshot.vendor_configs.emplace_back(std::move(cfg));
}
+89 -37
View File
@@ -4,6 +4,7 @@
#include "GUI_App.hpp"
#include "libslic3r/Preset.hpp"
#include "I18N.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <wx/colordlg.h>
#include <wx/dcgraph.h>
@@ -1075,54 +1076,105 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
// Sort the filaments
{
static std::unordered_map<wxString, int> sorted_names
{ {"Bambu PLA Basic", 0},
{"Bambu PLA Matte", 1},
{"Bambu PETG HF", 2},
{"Bambu ABS", 3},
{"Bambu PLA Silk", 4},
{"Bambu PLA-CF" , 5},
{"Bambu PLA Galaxy", 6},
{"Bambu PLA Metal", 7},
{"Bambu PLA Marble", 8},
{"Bambu PETG-CF", 9},
{"Bambu PETG Translucent", 10},
{"Bambu ABS-GF", 11}
std::unordered_map<wxString, int> selected_filament_ranks;
// Helper lambda to find a filament Preset by name. We can call this multiple times to walk the inheritance chain and find the base filament.
auto find_filament_by_name = [](const std::string& wanted, const PresetCollection& filaments) -> const Preset* {
for (auto it = filaments.begin(); it != filaments.end(); ++it) {
if (it->name == wanted) {
return &(*it);
}
}
return nullptr;
};
static std::vector<wxString> sorted_vendors { "Bambu Lab", "Generic" };
static std::vector<wxString> sorted_types { "PLA", "PETG", "ABS", "TPU" };
auto _filament_sorter = [&query_filament_vendors, &query_filament_types](const wxString& left, const wxString& right) -> bool
{
{ // Compare name order
const auto& iter1 = sorted_names.find(left);
int name_order1 = (iter1 != sorted_names.end()) ? iter1->second : INT_MAX;
// For each active filament preset, find its base filament alias and promote it in extruder order.
auto bundle = wxGetApp().preset_bundle;
const auto& preset_names = bundle->filament_presets;
for (size_t i = preset_names.size(); i-- > 0; ) {
std::string wanted = preset_names[i];
const int sort_rank = -static_cast<int>(preset_names.size() - i);
const Preset* match = nullptr;
const auto& iter2 = sorted_names.find(right);
int name_order2 = (iter2 != sorted_names.end()) ? iter2->second : INT_MAX;
if (name_order1 != name_order2)
do {
auto find_result = find_filament_by_name(wanted, bundle->filaments);
if (!find_result) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " No available filament name matches " << wanted;
break;
}
match = find_result;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found available filament matching current preset name " << wanted
<< " - Name: " << match->name << " - Alias: " << match->alias
<< " - Inherits: " << match->inherits();
if (match->inherits().length() == 0) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No more inherits so we reached the base filament";
break;
}
wanted = match->inherits();
} while (1); // Or loop while (match->alias.length() == 0) because existence of alias and inherits on a Preset seem to be exclusive
if (!match) {
continue;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Update filament rank to " + std::to_string(sort_rank) + " for preset Name: "
<< match->name << " - Alias: " << match->alias;
selected_filament_ranks.insert_or_assign(match->alias, sort_rank);
}
static const std::vector<wxString> sorted_vendors { "Generic" };
static const std::vector<wxString> sorted_types { "PLA", "PETG", "ABS", "TPU" };
auto priority_rank = [](const std::vector<wxString>& priorities, const wxString& value) {
const auto iter = std::find_if(priorities.begin(), priorities.end(), [&value](const wxString& priority) {
return priority.CmpNoCase(value) == 0;
});
return iter - priorities.begin();
};
auto _filament_sorter = [&query_filament_vendors, &query_filament_types, &selected_filament_ranks, &priority_rank](const wxString& left, const wxString& right) -> bool
{
{ // Compare selected filament order
const auto& iter1 = selected_filament_ranks.find(left);
int selected_order1 = (iter1 != selected_filament_ranks.end()) ? iter1->second : INT_MAX;
const auto& iter2 = selected_filament_ranks.find(right);
int selected_order2 = (iter2 != selected_filament_ranks.end()) ? iter2->second : INT_MAX;
if (selected_order1 != selected_order2)
{
return name_order1 < name_order2;
return selected_order1 < selected_order2;
}
}
{ // Compare vendor
auto iter1 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[left]);
auto iter2 = std::find(sorted_vendors.begin(), sorted_vendors.end(), query_filament_vendors[right]);
if (iter1 != iter2)
{
return iter1 < iter2;
};
const wxString& vendor1 = query_filament_vendors.at(left);
const wxString& vendor2 = query_filament_vendors.at(right);
const auto rank1 = priority_rank(sorted_vendors, vendor1);
const auto rank2 = priority_rank(sorted_vendors, vendor2);
if (rank1 != rank2)
return rank1 < rank2;
const int vendor_compare = vendor1.CmpNoCase(vendor2);
if (vendor_compare != 0)
return vendor_compare < 0;
}
{ // Compare type
auto iter1 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[left]);
auto iter2 = std::find(sorted_types.begin(), sorted_types.end(), query_filament_types[right]);
if (iter1 != iter2)
{
return iter1 < iter2;
}
const wxString& type1 = query_filament_types.at(left);
const wxString& type2 = query_filament_types.at(right);
const auto rank1 = priority_rank(sorted_types, type1);
const auto rank2 = priority_rank(sorted_types, type2);
if (rank1 != rank2)
return rank1 < rank2;
const int type_compare = type1.CmpNoCase(type2);
if (type_compare != 0)
return type_compare < 0;
}
return left < right;
const int name_compare = left.CmpNoCase(right);
return name_compare != 0 ? name_compare < 0 : left < right;
};
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
+5 -5
View File
@@ -869,11 +869,11 @@ void AuxiliaryPanel::init_tabpanel()
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), "", true);
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), "", false);
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), "", false);
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), "", false);
m_tabpanel->AddPage(m_others_panel, _L("Others"), "", false);
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false);
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false);
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
}
wxWindow *AuxiliaryPanel::create_side_tools()
-1
View File
@@ -488,7 +488,6 @@ void CalibrationPanel::init_tabpanel() {
selected = true;
m_tabpanel->AddPage(m_cali_panels[i],
get_calibration_type_name(m_cali_panels[i]->get_calibration_mode()),
"",
selected);
}
+1 -1
View File
@@ -752,7 +752,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0;
bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
bool has_solid_infill = has_top_shell_layers || has_bottom_shell;
toggle_line("sparse_infill_smooth_factor", pattern == ipHilbertCurve);
toggle_line("sparse_infill_smooth_factor", is_smoothable_infill_pattern(pattern, config->opt_int("fill_multiline")));
toggle_field("top_surface_pattern", has_top_shell);
toggle_field("bottom_surface_pattern", has_bottom_shell);
toggle_field("top_surface_density", has_top_shell_layers);
+29 -39
View File
@@ -66,41 +66,41 @@ using Config::SnapshotDB;
// Configuration data structures extensions needed for the wizard
//BBS: set BBL as default
bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle)
bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle)
{
this->preset_bundle = std::make_unique<PresetBundle>();
this->is_in_resources = ais_in_resources;
this->is_bbl_bundle = ais_bbl_bundle;
std::string path_string = source_path.string();
std::string parent_path = source_path.parent_path().string();
//BBS: add json logic for vendor bundles
std::string vendor_name = source_path.filename().string();
if (Slic3r::is_json_file(path_string)) {
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
}
else
// Orca: served from the vendor's preset cache where one covers it — which is
// how a shipped build carries its vendors — and parsed from the JSONs otherwise.
// A vendor that can be neither read nor parsed — a cache the build cannot use
// with the preset JSONs behind it pruned, say — is one the wizard cannot offer.
// Every other vendor still can be, so it is left out rather than thrown over.
size_t presets_loaded = 0;
try {
auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json(
dir.string(), vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
presets_loaded = loaded;
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what();
return false;
// Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air.
//BBS: add json logic for vendor bundles
auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json(
parent_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
}
auto first_vendor = preset_bundle->vendors.begin();
if (first_vendor == preset_bundle->vendors.end()) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string;
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name;
return false;
}
if (presets_loaded == 0) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string;
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name;
return false;
}
}
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded;
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded;
this->vendor_profile = &first_vendor->second;
return true;
}
@@ -125,15 +125,10 @@ BundleMap BundleMap::load()
//Orca: add custom as default
//Orca: add json logic for vendor bundle
auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
auto orca_bundle_rsrc = false;
if (!boost::filesystem::exists(orca_bundle_path)) {
orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
orca_bundle_rsrc = true;
}
{
const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE);
Bundle bbl_bundle;
if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true))
if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true))
res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle));
}
@@ -141,18 +136,13 @@ BundleMap BundleMap::load()
// and then additionally from resources/profiles.
bool is_in_resources = false;
for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) {
for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) {
//BBS: add json logic for vendor bundle
if (Slic3r::is_json_file(dir_entry.path().string())) {
std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part
for (const std::string &id : vendor_names_in(*dir)) {
// Don't load this bundle if we've already loaded it.
if (res.find(id) != res.end()) { continue; }
// Don't load this bundle if we've already loaded it.
if (res.find(id) != res.end()) { continue; }
Bundle bundle;
if (bundle.load(dir_entry.path(), is_in_resources))
res.emplace(std::move(id), std::move(bundle));
}
Bundle bundle;
if (bundle.load(*dir, id, is_in_resources))
res.emplace(id, std::move(bundle));
}
is_in_resources = true;
+3 -1
View File
@@ -71,9 +71,11 @@ struct Bundle
Bundle() = default;
Bundle(Bundle&& other);
// Load the vendor `vendor_name` as it is installed in `dir`, from its preset
// cache or its profile JSONs, whichever is usable.
// Returns false if not loaded. Reason for that is logged as boost::log error.
//BBS: set BBL as default
bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false);
bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false);
const std::string& vendor_id() const { return vendor_profile->id; }
};
+3 -1
View File
@@ -156,6 +156,8 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
{
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
if (code.empty())
code = "88888888";
for (char c : code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
show_error(this, _L("Invalid input"));
@@ -163,7 +165,7 @@ void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
}
}
if (m_obj) {
m_obj->set_user_access_code(code.ToStdString());
m_obj->set_access_code(code.ToStdString());
}
EndModal(wxID_OK);
}
+4 -15
View File
@@ -2201,25 +2201,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre
} else {
selected_vendor_id = m_printer_preset_vendor_selected.id;
if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string();
} else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string();
}
if (preset_path.empty()) {
BOOST_LOG_TRIVIAL(info) << "Preset path was not found";
MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES_NO | wxYES_DEFAULT | wxCENTRE);
dlg.ShowModal();
return false;
}
try {
// Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base
// bundle so vendor filaments that inherit OFL bases resolve via the existing
// cross-vendor inheritance path.
temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id,
// Orca: served from the vendor's preset cache where one covers it — a shipped
// build carries that instead of the raw preset JSONs — and parsed otherwise.
temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(),
selected_vendor_id,
PresetBundle::LoadConfigBundleAttribute::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent,
wxGetApp().preset_bundle);
+75 -17
View File
@@ -10,11 +10,39 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp"
using namespace nlohmann;
namespace {
// Orca: access_code lives on BBLocalMachine::access_code (keyed by dev_id via
// get_local_machines(), scoped by the record's own printer_agent_id field) - so binding a
// printer under one agent doesn't silently appear as already-bound under a different,
// independent agent. This only covers LAN devices (BBLocalMachine's own scope); access_code
// and user_access_code used to be the only, flat dev_id-only AppConfig keys before
// BBLocalMachine::access_code existed, and codes saved back then are still stored flat (no
// agent association at all). Since BBL was the only agent that existed at the time, honor
// those flat legacy keys as implicitly BBL's - but only for the BBL agent, so they aren't
// leaked to other agents that never bound the device themselves.
std::string get_access_code_with_legacy_fallback(Slic3r::AppConfig* config, const std::string& dev_id, const std::string& agent_id)
{
const auto& machines = config->get_local_machines();
auto it = machines.find(dev_id);
if (it != machines.end() && it->second.printer_agent_id == agent_id && !it->second.access_code.empty())
return it->second.access_code;
if (agent_id == Slic3r::BBL_PRINTER_AGENT_ID || agent_id.empty()) {
std::string code = config->get("access_code", dev_id);
if (code.empty())
code = config->get("user_access_code", dev_id);
return code;
}
return "";
}
}
namespace Slic3r
{
DeviceManager::DeviceManager(NetworkAgent* agent)
@@ -43,13 +71,13 @@ namespace Slic3r
continue;
MachineObject* obj = new MachineObject(this, m_agent, m.dev_name, m.dev_id, m.dev_ip);
obj->printer_type = m.printer_type;
obj->printer_agent_id = m.printer_agent_id;
obj->dev_connection_type = "lan";
obj->bind_state = "free";
obj->bind_sec_link = "secure";
obj->m_is_online = true;
obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->set_access_code(config->get("access_code", m.dev_id), false);
obj->set_user_access_code(config->get("user_access_code", m.dev_id), false);
obj->set_access_code(get_access_code_with_legacy_fallback(config, m.dev_id, obj->printer_agent_id), false);
if (obj->has_access_right()) {
localMachineList.insert(std::make_pair(m.dev_id, obj));
} else {
@@ -66,10 +94,12 @@ namespace Slic3r
if (m.is_lan_mode_printer()) {
if (m.has_access_right()) {
BBLocalMachine local_machine;
local_machine.dev_id = m.get_dev_id();
local_machine.dev_name = m.get_dev_name();
local_machine.dev_ip = m.get_dev_ip();
local_machine.printer_type = m.printer_type;
local_machine.dev_id = m.get_dev_id();
local_machine.dev_name = m.get_dev_name();
local_machine.dev_ip = m.get_dev_ip();
local_machine.printer_type = m.printer_type;
local_machine.printer_agent_id = m.printer_agent_id;
local_machine.access_code = m.get_access_code();
config->update_local_machine(local_machine);
}
} else {
@@ -132,6 +162,14 @@ namespace Slic3r
}
}
std::string DeviceManager::get_current_printer_agent_id() const
{
if (!m_agent)
return "";
auto printer_agent = m_agent->get_printer_agent();
return printer_agent ? printer_agent->get_agent_info().id : "";
}
void DeviceManager::EnableMultiMachine(bool enable)
{
m_agent->enable_multi_machine(enable);
@@ -328,6 +366,7 @@ namespace Slic3r
/* insert a new machine */
obj = new MachineObject(this, m_agent, dev_name, dev_id, dev_ip);
obj->printer_type = _parse_printer_type(printer_type_str);
obj->printer_agent_id = get_current_printer_agent_id();
obj->wifi_signal = printer_signal;
obj->dev_connection_type = connect_type;
obj->bind_state = bind_state;
@@ -339,8 +378,7 @@ namespace Slic3r
//load access code
AppConfig* config = Slic3r::GUI::wxGetApp().app_config;
if (config) {
obj->set_access_code(Slic3r::GUI::wxGetApp().app_config->get("access_code", dev_id), false);
obj->set_user_access_code(Slic3r::GUI::wxGetApp().app_config->get("user_access_code", dev_id), false);
obj->set_access_code(get_access_code_with_legacy_fallback(config, dev_id, obj->printer_agent_id), false);
}
localMachineList.insert(std::make_pair(dev_id, obj));
@@ -369,6 +407,7 @@ namespace Slic3r
obj = it->second;
} else {
obj = new MachineObject(this, m_agent, machine.dev_name, machine.dev_id, machine.dev_ip);
obj->printer_agent_id = get_current_printer_agent_id();
localMachineList.insert(std::make_pair(machine.dev_id, obj));
}
if (machine.printer_type.empty())
@@ -382,7 +421,6 @@ namespace Slic3r
obj->m_is_online = true;
obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->set_access_code(access_code, false);
obj->set_user_access_code(access_code, false);
update_local_machine(*obj);
@@ -496,16 +534,26 @@ namespace Slic3r
OnSelectedMachineChanged(previous_selected_machine, selected_machine);
}
void DeviceManager::clear_other_devices()
void DeviceManager::clear_other_devices(const std::string& target_agent_id)
{
// why: on agent swap, keep "My Devices" but drop the transient "Other Devices"
// Those belong to the previous agent's network scan; the new agent's start_discovery re-populates its own.
//
// Also drop "My Devices" stamped by a different agent than the one we're swapping to
// (target_agent_id, passed by the caller since the live agent hasn't been repointed yet
// at this point): otherwise a device first discovered under agent A survives every swap
// with a stale printer_agent_id, stays hidden from every agent's filtered list, and only
// gets re-tagged if something happens to delete and re-create it (e.g. account logout).
// Dropping it here instead lets the new agent's start_discovery re-insert and re-stamp it
// like any other fresh device.
const auto my = get_my_machine_list();
for (auto it = localMachineList.begin(); it != localMachineList.end();)
{
if (my.find(it->first) == my.end())
const bool is_my_device = my.find(it->first) != my.end();
const bool agent_mismatch = !target_agent_id.empty() && it->second &&
it->second->printer_agent_id != target_agent_id;
if (!is_my_device || agent_mismatch)
{
// not a "My Device" -> an "Other Device"
delete it->second;
it = localMachineList.erase(it);
}
@@ -688,13 +736,16 @@ namespace Slic3r
m_agent->add_subscribe(subscribe_list_cache);
}
std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list()
std::map<std::string, MachineObject*> DeviceManager::get_my_machine_list(const std::string& agent_id)
{
std::map<std::string, MachineObject*> result;
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
{
if (it->second && !it->second->is_lan_mode_printer())
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (!it->second->is_lan_mode_printer())
{
result.insert(std::make_pair(it->first, it->second));
}
@@ -702,7 +753,10 @@ namespace Slic3r
for (auto it = localMachineList.begin(); it != localMachineList.end(); it++)
{
if (it->second && it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer())
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (it->second->has_access_right() && it->second->is_avaliable() && it->second->is_lan_mode_printer())
{
// remove redundant in userMachineList
if (result.find(it->first) == result.end())
@@ -714,12 +768,15 @@ namespace Slic3r
return result;
}
std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list()
std::map<std::string, MachineObject*> DeviceManager::get_my_cloud_machine_list(const std::string& agent_id)
{
std::map<std::string, MachineObject*> result;
for (auto it = userMachineList.begin(); it != userMachineList.end(); it++)
{
if (it->second && !it->second->is_lan_mode_printer()) { result.emplace(*it); }
if (!it->second || (!agent_id.empty() && it->second->printer_agent_id != agent_id))
continue;
if (!it->second->is_lan_mode_printer()) { result.emplace(*it); }
}
return result;
}
@@ -792,6 +849,7 @@ namespace Slic3r
else
{
obj = new MachineObject(this, m_agent, "", "", "");
obj->printer_agent_id = get_current_printer_agent_id();
if (m_agent)
{
obj->set_bind_status(m_agent->get_user_name(provider));
+11 -3
View File
@@ -74,7 +74,10 @@ public:
void erase_user_machine(std::string dev_id) { userMachineList.erase(dev_id); }
void clean_user_info(bool keep_local_selection = false);
void clear_other_devices();
// target_agent_id: id of the agent being swapped to (empty = no agent-mismatch check,
// just the original "drop Other Devices" behavior). Pass the incoming agent's id, not the
// live one - this runs before the live agent is repointed.
void clear_other_devices(const std::string& target_agent_id = "");
void load_last_machine();
void update_user_machine_list_info(const std::string& provider);
@@ -90,10 +93,15 @@ public:
/* my machine*/
MachineObject* get_my_machine(std::string dev_id);
std::map<std::string, MachineObject*> get_my_machine_list();
std::map<std::string, MachineObject*> get_my_cloud_machine_list();
std::map<std::string, MachineObject*> get_my_machine_list(const std::string& agent_id = "");
std::map<std::string, MachineObject*> get_my_cloud_machine_list(const std::string& agent_id = "");
void modify_device_name(std::string dev_id, std::string dev_name, const std::string& provider);
// id of the currently live IPrinterAgent (IPrinterAgent::get_agent_info().id), or empty if
// m_agent has no printer agent set yet. Pass to get_my_machine_list()/get_my_cloud_machine_list()
// to scope results to the active agent.
std::string get_current_printer_agent_id() const;
/* create machine or update machine properties */
void on_machine_alive(std::string json_str);
int query_bind_status(std::string& msg, const std::string& provider);
+36 -39
View File
@@ -3,6 +3,7 @@
#include "libslic3r/Time.hpp"
#include "libslic3r/Thread.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "GuiColor.hpp"
#include "GUI_App.hpp"
@@ -449,9 +450,7 @@ bool MachineObject::HasRecentLanMessage()
std::string MachineObject::get_access_code() const
{
if (get_user_access_code().empty())
return access_code;
return get_user_access_code();
return access_code;
}
void MachineObject::set_access_code(std::string code, bool only_refresh)
@@ -460,47 +459,46 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
if (only_refresh) {
AppConfig* config = GUI::wxGetApp().app_config;
if (config) {
if (!code.empty()) {
GUI::wxGetApp().app_config->set_str("access_code", get_dev_id(), code);
DeviceManager::update_local_machine(*this);
if (is_lan_mode_printer()) {
// why: LAN codes are scoped via BBLocalMachine::access_code, keyed by dev_id and
// scoped by that record's own printer_agent_id field - see the matching comment
// on get_access_code_with_legacy_fallback() in DevManager.cpp - so binding this
// device under one printer agent doesn't silently read as already-bound under a
// different, independent one. Cloud devices (the else branch below) aren't
// scoped this way: they're never recalled from a stale local cache across a
// session boundary, since parse_user_print_info() always overwrites their code
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
DeviceManager::update_local_machine(*this);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
const auto& machines = config->get_local_machines();
auto it = machines.find(get_dev_id());
if (it != machines.end()) {
BBLocalMachine local_machine = it->second;
local_machine.access_code = "";
config->update_local_machine(local_machine);
}
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
// old BBL-era code can't silently "re-bind" this device again via
// get_access_code_with_legacy_fallback()'s legacy fallback.
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
config->erase("access_code", get_dev_id());
config->erase("user_access_code", get_dev_id());
}
}
} else {
GUI::wxGetApp().app_config->erase("access_code", get_dev_id());
if (!code.empty())
config->set_str("access_code", get_dev_id(), code);
else
config->erase("access_code", get_dev_id());
}
}
}
}
void MachineObject::erase_user_access_code()
{
this->user_access_code = "";
AppConfig* config = GUI::wxGetApp().app_config;
if (config) {
GUI::wxGetApp().app_config->erase("user_access_code", get_dev_id());
//GUI::wxGetApp().app_config->save();
}
}
void MachineObject::set_user_access_code(std::string code, bool only_refresh)
{
this->user_access_code = code;
if (only_refresh && !code.empty()) {
AppConfig* config = GUI::wxGetApp().app_config;
if (config && !code.empty()) {
GUI::wxGetApp().app_config->set_str("user_access_code", get_dev_id(), code);
DeviceManager::update_local_machine(*this);
}
}
}
std::string MachineObject::get_user_access_code() const
{
AppConfig* config = GUI::wxGetApp().app_config;
if (config) {
return GUI::wxGetApp().app_config->get("user_access_code", get_dev_id());
}
return "";
}
std::string MachineObject::get_show_printer_type() const
{
std::string printer_type = this->printer_type;
@@ -2915,7 +2913,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
std::string access_code = j_pre["system"]["access_code"].get<std::string>();
if (!access_code.empty()) {
set_access_code(access_code);
set_user_access_code(access_code);
}
}
}
+10 -6
View File
@@ -113,7 +113,6 @@ private:
std::string dev_name;
std::string dev_ip;
std::string access_code;
std::string user_access_code;
// type, time stamp, delay
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
@@ -228,13 +227,18 @@ public:
std::string get_access_code() const;
void set_access_code(std::string code, bool only_refresh = true);
/*user access code*/
void set_user_access_code(std::string code, bool only_refresh = true);
void erase_user_access_code();
std::string get_user_access_code() const;
//PRINTER_TYPE printer_type = PRINTER_3DPrinter_UKNOWN;
std::string printer_type; /* model_id */
// id of the IPrinterAgent that was used to discover or bind this device (IPrinterAgent::get_agent_info().id,
// e.g. "bbl"), stamped at creation time — not derived from get_agent(), since m_agent is a single
// process-wide NetworkAgent shared by every MachineObject and gets repointed on agent swap
// (see DeviceManager::set_agent()), so it can't tell which agent originally found this device.
// We persist this as well so that when the printer agent is swapped, we don't show unrelated devices,
// e.g. if the current printer agent is elegoo, we shouldn't show printers connected by BBL printer agent
// under local machines.
std::string printer_agent_id;
std::string get_show_printer_type() const;
PrinterSeries get_printer_series() const;
PrinterArch get_printer_arch() const;
+1 -1
View File
@@ -134,7 +134,7 @@ void Downloader::start_download(const std::string& full_url)
Plater* plater = wxGetApp().plater();
mainframe->Freeze();
mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
mainframe->select_tab(TAB_ID_PREPARE);
plater->select_view_3D("3D");
plater->select_view("plate");
plater->get_current_canvas3D()->zoom_to_bed();
+4 -2
View File
@@ -331,8 +331,10 @@ void Field::PostInitialize()
}
default: break;
}
if (tab_id >= 0)
wxGetApp().mainframe->select_tab(tab_id);
if (tab_id >= 0) {
static constexpr const char* kShortcutTabIds[] = {TAB_ID_HOME, TAB_ID_PREPARE, TAB_ID_PREVIEW, TAB_ID_MONITOR};
wxGetApp().mainframe->select_tab(kShortcutTabIds[tab_id]);
}
if (tab_id > 0)
// tab panel should be focused for correct navigation between tabs
wxGetApp().tab_panel()->SetFocus();
+9 -20
View File
@@ -2887,7 +2887,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id);
float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id);
float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value;
float a = dynamic_cast<const ConfigOptionFloat*>(proj_cfg.option("wipe_tower_rotation_angle"))->value;
float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
// BBS
float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value;
Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin();
@@ -9196,7 +9196,7 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
view3d_canvas->get_gizmos_manager().reset_all_states(); // close all gizmos
view3d_canvas->reload_scene(true);
}
app.mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
app.mainframe->select_tab(TAB_ID_PREPARE);
}
}
});
@@ -10602,9 +10602,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
wxString region = L"en";
if (language.find("zh") == 0)
region = L"zh";
// Use the generic dual-nozzle PLA+PETG guide rather than the H2D-specific page
// so the link is relevant for all dual-extrusion printers, not just Bambu H2D. (#12073)
wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/pla-and-petg-dual-extrusion", region));
// Although this link looks like it's only for the H2D, its guidance is generic.
wxGetApp().open_browser_with_warning_dialog(wxString::Format(L"https://wiki.bambulab.com/%s/filament-acc/filament/h2d-pla-and-petg-mutual-support", region));
return false;
});
}
@@ -10738,24 +10737,14 @@ bool GLCanvas3D::is_flushing_matrix_error() {
if (!Sidebar::should_show_SEMM_buttons())
return false;
std::vector<int> plate_extruders = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_extruders(true);
if (plate_extruders.size() < 2)
return false;
const auto &project_config = wxGetApp().preset_bundle->project_config;
const std::vector<double> &config_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values;
const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->values;
for (auto multiplier : config_multiplier) {
if (multiplier == 0) return true;
}
int matrix_len = config_matrix.size() / config_multiplier.size();
int row_len = std::sqrt(matrix_len);
for (int i = 0; i < config_matrix.size(); i++)
{
int relative_id = i % matrix_len;
int row_id = relative_id / row_len;
int col_id = relative_id % row_len;
if (row_id != col_id && config_matrix[i] == 0) return true;
}
return false;
return has_zero_flush_volume_for_used_filaments(config_matrix, config_multiplier, plate_extruders);
}
bool GLCanvas3D::_is_any_volume_outside() const
+45 -21
View File
@@ -813,12 +813,12 @@ void GUI_App::post_init()
m_open_method = "url";
} else {
if (this->init_params->input_gcode) {
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
this->plater()->load_gcode(from_u8(this->init_params->input_files.front()));
m_open_method = "gcode";
} else {
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
wxArrayString input_files;
for (auto& file : this->init_params->input_files) {
@@ -852,7 +852,7 @@ void GUI_App::post_init()
mainframe->Freeze();
#endif
plater_->canvas3D()->enable_render(false);
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
mainframe->select_tab(TAB_ID_PREPARE);
plater_->select_view_3D("3D");
//BBS init the opengl resource here
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
@@ -890,9 +890,9 @@ void GUI_App::post_init()
}
}
if (is_editor())
mainframe->select_tab(size_t(0));
mainframe->select_tab(TAB_ID_HOME);
if (app_config->get("default_page") == "1")
mainframe->select_tab(size_t(1));
mainframe->select_tab(TAB_ID_PREPARE);
#ifndef __linux__
mainframe->Thaw();
#endif
@@ -1829,10 +1829,10 @@ bool GUI_App::hot_reload_network_plugin()
wxWindowDisabler disabler;
if (mainframe) {
int current_tab = mainframe->m_tabpanel->GetSelection();
if (current_tab == MainFrame::TabPosition::tpMonitor) {
wxString current_tab = mainframe->m_tabpanel->GetSelectedPageName();
if (current_tab == TAB_ID_MONITOR) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": navigating away from Monitor tab before unload";
mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tp3DEditor);
mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREPARE);
}
}
@@ -2166,7 +2166,6 @@ void GUI_App::init_networking_callbacks()
obj->is_tunnel_mqtt = tunnel;
obj->command_request_push_all(true);
obj->command_get_version();
obj->erase_user_access_code();
obj->command_get_access_code();
if (m_agent)
m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
@@ -2216,7 +2215,6 @@ void GUI_App::init_networking_callbacks()
wxString text;
if (msg == "5") {
obj->set_access_code("");
obj->erase_user_access_code();
text = wxString::Format(_L("Incorrect password"));
wxGetApp().show_dialog(text);
} else {
@@ -2853,6 +2851,16 @@ void GUI_App::init_plugin_gui_wiring()
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.subscribe_on_load_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
return;
wxGetApp().mainframe->plugin_pages().on_plugin_register(plugin_key);
});
plugin_mgr.subscribe_on_unload_callback([](const std::string& plugin_key) {
if (wxTheApp == nullptr || wxGetApp().is_closing() || wxGetApp().mainframe == nullptr)
return;
wxGetApp().mainframe->plugin_pages().on_plugin_deregister(plugin_key);
});
plugin_mgr.subscribe_on_load_callback(refresh_printer_agent_dropdown_after_load);
plugin_mgr.subscribe_on_unload_callback(switch_printer_agent_after_unload);
plugin_mgr.subscribe_on_capability_load_callback(
@@ -2868,11 +2876,15 @@ void GUI_App::init_plugin_gui_wiring()
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
wxGetApp().mainframe->plugin_pages().on_cap_register(capability);
});
plugin_mgr.subscribe_on_capability_unload_callback(
[refresh_plugins_dialog, switch_printer_agent_after_unload](const PluginCapabilityId& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
if (capability.type == PluginCapabilityType::Pages && wxTheApp && !wxGetApp().is_closing() && wxGetApp().mainframe)
wxGetApp().mainframe->plugin_pages().on_cap_deregister(capability);
refresh_plugins_dialog();
switch_printer_agent_after_unload(capability.plugin_key);
});
@@ -3275,15 +3287,12 @@ bool GUI_App::on_init_inner()
}
} */
copy_network_if_available();
if (scrn) {
scrn->SetText(_L("Loading Plugins") + dots, 20);
wxYield();
}
on_init_network();
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
// initialize() also installs the libslic3r hooks (capability resolver,
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no
@@ -3312,6 +3321,9 @@ bool GUI_App::on_init_inner()
}
}
copy_network_if_available();
on_init_network();
if (m_agent)
plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()));
@@ -3384,7 +3396,7 @@ bool GUI_App::on_init_inner()
mainframe = new MainFrame();
// hide settings tabs after first Layout
if (is_editor()) {
mainframe->select_tab(size_t(0));
mainframe->select_tab(TAB_ID_HOME);
}
sidebar().obj_list()->init();
@@ -3939,7 +3951,13 @@ void GUI_App::set_live_printer_agent(std::shared_ptr<IPrinterAgent> agent)
m_agent->set_user_selected_machine("");
// note: belt-and-suspenders (precedent: DeviceManagerRefresher::on_timer)
dev->OnSelectedMachineLost(); // why: clear stale sidebar sync-status / AMS
dev->clear_other_devices(); // why: drop stale LAN discoveries; keep My Devices
// why: drop stale LAN discoveries; keep My Devices, but only those belonging to the
// agent we're about to swap to, so a device stamped by the outgoing agent doesn't
// linger hidden - the new agent's start_discovery re-inserts and re-stamps it fresh.
// agent is null when clearing the live agent entirely (e.g. plugin unload); there's no
// target to filter against then, so fall back to the original "keep all My Devices"
// behavior rather than guessing.
dev->clear_other_devices(agent ? agent->get_agent_info().id : std::string());
}
m_agent->set_printer_agent(agent);
@@ -4592,7 +4610,7 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
mainframe = new MainFrame();
if (is_editor())
// hide settings tabs after first Layout
mainframe->select_tab(size_t(MainFrame::tp3DEditor));
mainframe->select_tab(TAB_ID_PREPARE);
// Propagate model objects to object list.
sidebar().obj_list()->init();
//sidebar().aux_list()->init_auxiliary();
@@ -6799,6 +6817,12 @@ void GUI_App::add_pending_vendor_preset(const std::pair<std::string, std::map<st
// Add the corresponding vendor
std::string vendor_name = PresetBundle::find_preset_vendor(inherits_name, type);
if (vendor_name.empty()) {
// No vendor ships this preset's parent. An unnamed entry here becomes an
// unnamed bundle at install time, which nothing can install.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no vendor carries " << inherits_name << ", skipping";
return;
}
if (need_add_vendors.find(vendor_name) == need_add_vendors.end())
need_add_vendors[vendor_name] = std::map<std::string, std::set<std::string>>();
@@ -8286,7 +8310,7 @@ bool GUI_App::show_modal_ip_address_enter_dialog(bool input_sn, wxString title)
wxGetApp().app_config->save();
obj->set_dev_ip(ip_address.ToStdString());
obj->set_user_access_code(access_code.ToStdString());
obj->set_access_code(access_code.ToStdString());
}
}
});
@@ -9851,7 +9875,7 @@ bool GUI_App::check_url_association(std::wstring url_prefix, std::wstring& reg_b
{
reg_bin = L"";
#ifdef WIN32
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_full.Exists()) {
return false;
}
@@ -9877,8 +9901,8 @@ void GUI_App::associate_url(std::wstring url_prefix)
wxString key_string = "\"" + wbinary + "\" \"%1\"";
wxRegKey key_first(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix);
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
wxRegKey key_first(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix);
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_first.Exists()) {
key_first.Create(false);
}
@@ -9898,7 +9922,7 @@ void GUI_App::disassociate_url(std::wstring url_prefix)
#ifdef WIN32
if (is_running_in_msix())
return;
wxRegKey key_full(wxRegKey::HKCU, "Software\\Classes\\" + url_prefix + "\\shell\\open\\command");
wxRegKey key_full(wxRegKey::HKCU, L"Software\\Classes\\" + url_prefix + L"\\shell\\open\\command");
if (!key_full.Exists()) {
return;
}
+1
View File
@@ -1,6 +1,7 @@
#include "HMS.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "DeviceManager.hpp"
#include "DeviceCore/DevManager.h"
#include "DeviceCore/DevUtil.h"
+16 -13
View File
@@ -1,7 +1,6 @@
#ifndef slic3r_HMS_hpp_
#define slic3r_HMS_hpp_
#include "GUI_App.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include "Widgets/Label.hpp"
@@ -11,7 +10,11 @@
#include "slic3r/Utils/Http.hpp"
#include "libslic3r/Thread.hpp"
#include "nlohmann/json.hpp"
#include <ctime>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r {
@@ -26,12 +29,12 @@ namespace GUI {
class HMSQuery {
protected:
std::unordered_map<string, json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number
std::unordered_map<string, json> m_hms_action_jsons;// key-> device id type
std::unordered_map<std::string, nlohmann::json> m_hms_info_jsons; // key-> device id type, the first three digits of SN number
std::unordered_map<std::string, nlohmann::json> m_hms_action_jsons;// key-> device id type
std::unordered_map<wxString, wxImage> m_hms_local_images; // key-> image name
mutable std::mutex m_hms_mutex;
std::unordered_map<string, time_t> m_cloud_hms_last_update_time;
std::unordered_map<std::string, std::time_t> m_cloud_hms_last_update_time;
public:
HMSQuery() { }
@@ -61,18 +64,18 @@ private:
// load hms
void init_hms_info(const std::string& dev_type_id);
void copy_from_data_dir_to_local();
int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, json* receive_json);
int load_from_local(const std::string& hms_type, const std::string& dev_id_type, json* receive_json, std::string& version_info);
int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, json save_json);
int download_hms_related(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json);
int load_from_local(const std::string& hms_type, const std::string& dev_id_type, nlohmann::json* receive_json, std::string& version_info);
int save_to_local(std::string lang, std::string hms_type, std::string dev_id_type, nlohmann::json save_json);
std::string get_hms_file(std::string hms_type, std::string lang = std::string("en"), std::string dev_id_type = "");
// internal query
string get_dev_id_type(const MachineObject* obj) const;
wxString _query_hms_msg(const string& dev_id_type, const string& long_error_code, const string& lang_code = std::string("en"));
std::string get_dev_id_type(const MachineObject* obj) const;
wxString _query_hms_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
bool _is_internal_error(const string &dev_id_type, const string &long_error_code, const string &lang_code = std::string("en"));
wxString _query_error_msg(const string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
wxString _query_error_image_action(const string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action);
bool _is_internal_error(const std::string &dev_id_type, const std::string &long_error_code, const std::string &lang_code = std::string("en"));
wxString _query_error_msg(const std::string& dev_id_type, const std::string& long_error_code, const std::string& lang_code = std::string("en"));
wxString _query_error_image_action(const std::string& dev_id_type, const std::string& long_error_code, std::vector<int>& button_action);
};
int get_hms_info_version(std::string &version);
@@ -85,4 +88,4 @@ std::string get_error_message(int error_code);
}
#endif
#endif
+14
View File
@@ -2809,12 +2809,26 @@ void ImGuiWrapper::init_font(bool compress)
}
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 };
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
}
bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data);
if (bold_font == nullptr) {
bold_font = io.Fonts->AddFontDefault();
if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); }
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 };
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
}
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
default_font->Scale *= 1.25f;
bold_font->Scale *= 1.25f;
@@ -45,24 +45,26 @@ void CreateFontStyleImagesJob::process(Ctl &ctl)
for (const ExPolygon &shape : shapes)
bounding_box.merge(BoundingBox(shape.contour.points));
for (ExPolygon &shape : shapes) shape.translate(-bounding_box.min);
// calculate conversion from FontPoint to screen pixels by size of font
double scale = get_text_shape_scale(item.prop, *item.font.font_file) * m_input.ppm;
scales[index] = scale;
//double scale = font_prop.size_in_mm * SCALING_FACTOR;
BoundingBoxf bb2(bounding_box.min.cast<double>(),
bounding_box.max.cast<double>());
if (bounding_box.size().x() < 1 || bounding_box.size().y() < 1)
continue; // or however the font job's degenerate-box case is handled
// Normalize to fit max_size, exactly like CreateFontImageJob does against m_input.size.
// Fit by height (matches row height), then clamp width if needed.
constexpr float preview_padding_px = 2.f; // margin for AA sampling, tune to your AA kernel radius
double scale = m_input.max_size.y() / (double) bounding_box.size().y();
BoundingBoxf bb2(bounding_box.min.cast<double>(), bounding_box.max.cast<double>());
bb2.scale(scale);
image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x());
image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y());
// crop image width
if (image.tex_size.x > m_input.max_size.x())
// crop width only if the (now height-normalized) text is too wide
image.tex_size.x = std::ceil(bb2.max.x() - bb2.min.x()) + 2 * preview_padding_px;
image.tex_size.y = std::ceil(bb2.max.y() - bb2.min.y()) + 2 * preview_padding_px;
if (image.tex_size.x > m_input.max_size.x())
image.tex_size.x = m_input.max_size.x();
// crop image height
if (image.tex_size.y > m_input.max_size.y())
image.tex_size.y = m_input.max_size.y();
scales[index] = scale;
}
// arrange bounding boxes
+120 -96
View File
@@ -493,9 +493,8 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
});
//BBS
Bind(EVT_SELECT_TAB, [this](wxCommandEvent&evt) {
TabPosition pos = (TabPosition)evt.GetInt();
m_tabpanel->SetSelection(pos);
Bind(EVT_SELECT_TAB, [this](wxCommandEvent& evt) {
m_tabpanel->SelectPageByName(evt.GetString());
});
Bind(EVT_SYNC_CLOUD_PRESET, &MainFrame::on_select_default_preset, this);
@@ -702,7 +701,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#endif
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SetSelection(tpPreview); } return; }
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
m_print_enable = get_enable_print_status();
@@ -723,7 +722,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'S') { if (can_save_as()) m_plater->save_project(true); return;}
else if (evt.CmdDown() && evt.GetKeyCode() == 'S') { if (can_save()) m_plater->save_project(); return;}
if (evt.CmdDown() && evt.GetKeyCode() == 'F') {
if (m_plater && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview)) {
if (m_plater && is_prepare_or_preview_tab()) {
m_plater->sidebar().can_search();
}
}
@@ -1007,8 +1006,8 @@ void MainFrame::update_layout()
m_layout = layout;
// From the very beginning the Print settings should be selected
//m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? 0 : 1;
m_last_selected_tab = 1;
//m_last_selected_tab = m_layout == ESettingsLayout::Dlg ? TAB_ID_HOME : TAB_ID_PREPARE;
m_last_selected_tab = TAB_ID_PREPARE;
// Set new settings
switch (m_layout)
@@ -1016,14 +1015,18 @@ void MainFrame::update_layout()
case ESettingsLayout::Old:
{
m_plater->Reparent(m_tabpanel);
m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false);
m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false);
// Right after Home — or first, when there is no Home tab (PositionAfter() would
// append instead, and by now the other built-in tabs are already in place).
const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
m_tabpanel->Bind(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, [this](wxCommandEvent& evt)
{
// jump to 3deditor under preview_only mode
if (evt.GetId() == tp3DEditor){
if (evt.GetId() == m_tabpanel->FindPageByName(TAB_ID_PREPARE)) {
Sidebar& sidebar = GUI::wxGetApp().sidebar();
if (sidebar.need_auto_sync_after_connect_printer()) {
sidebar.set_need_auto_sync_after_connect_printer(false);
@@ -1107,6 +1110,9 @@ void MainFrame::update_edge_panels()
void MainFrame::shutdown()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "MainFrame::shutdown enter";
if (m_project != nullptr)
m_project->shutdown();
m_plugin_pages.shutdown();
#ifdef __WXGTK__
// Edge panels are child windows — wxWidgets destroys them automatically.
m_edge_bottom = nullptr;
@@ -1252,15 +1258,14 @@ void MainFrame::init_tabpanel() {
#endif
//BBS
wxWindow* panel = m_tabpanel->GetCurrentPage();
int sel = m_tabpanel->GetSelection();
//wxString page_text = m_tabpanel->GetPageText(sel);
m_last_selected_tab = m_tabpanel->GetSelection();
m_last_selected_tab = m_tabpanel->GetSelectedPageName();
if (panel == m_plater) {
if (sel == tp3DEditor) {
if (m_last_selected_tab == TAB_ID_PREPARE) {
wxPostEvent(m_plater, SimpleEvent(EVT_GLVIEWTOOLBAR_3D));
m_param_panel->OnActivate();
}
else if (sel == tpPreview) {
else if (m_last_selected_tab == TAB_ID_PREVIEW) {
m_plater->reset_check_status();
if (!m_plater->check_ams_status(m_slice_select == eSliceAll))
return;
@@ -1275,7 +1280,7 @@ void MainFrame::init_tabpanel() {
//monitor
}
#ifndef __APPLE__
if (sel == tp3DEditor) {
if (m_last_selected_tab == TAB_ID_PREPARE) {
m_topbar->EnableUndoRedoItems();
}
else {
@@ -1285,34 +1290,16 @@ void MainFrame::init_tabpanel() {
if (panel)
panel->SetFocus();
/*switch (sel) {
case TabPosition::tpHome:
show_option(false);
break;
case TabPosition::tp3DEditor:
show_option(true);
break;
case TabPosition::tpPreview:
show_option(true);
break;
case TabPosition::tpMonitor:
show_option(false);
break;
default:
show_option(false);
break;
}*/
});
if (wxGetApp().is_editor()) {
m_webview = new WebViewPanel(m_tabpanel);
Bind(EVT_LOAD_URL, [this](wxCommandEvent &evt) {
wxString url = evt.GetString();
select_tab(MainFrame::tpHome);
select_tab(TAB_ID_HOME);
m_webview->load_url(url);
});
m_tabpanel->AddPage(m_webview, "", "tab_home_active", "tab_home_active", false);
m_tabpanel->AddPage(TAB_ID_HOME, m_webview, "", "tab_home_active");
m_param_panel = new ParamsPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL);
}
@@ -1327,7 +1314,7 @@ void MainFrame::init_tabpanel() {
//BBS add pages
m_monitor = new MonitorPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_monitor->SetBackgroundColour(*wxWHITE);
m_tabpanel->AddPage(m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"), false);
m_tabpanel->AddPage(TAB_ID_MONITOR, m_monitor, _L("Device"), "tab_monitor_active");
m_printer_view = new PrinterWebView(m_tabpanel);
Bind(EVT_LOAD_PRINTER_URL, [this](LoadPrinterViewEvent &evt) {
@@ -1342,16 +1329,20 @@ void MainFrame::init_tabpanel() {
m_multi_machine = new MultiMachinePage(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_multi_machine->SetBackgroundColour(*wxWHITE);
// TODO: change the bitmap
m_tabpanel->AddPage(m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"), std::string("tab_multi_active"), false);
m_tabpanel->AddPage(TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
}
m_project = new ProjectPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_project->SetBackgroundColour(*wxWHITE);
m_tabpanel->AddPage(m_project, _L("Project"), std::string("tab_auxiliary_active"), std::string("tab_auxiliary_active"), false);
m_tabpanel->AddPage(TAB_ID_PROJECT, m_project, _L("Project"), "tab_auxiliary_active");
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"), std::string("tab_calibration_active"), false);
m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration, _L("Calibration"), "tab_calibration_active");
// Plugin pages are appended after the built-in tabs; their ids are namespaced
// (plugin.<plugin_key>.<name>) so they can't collide with the built-in TAB_ID_* constants.
m_plugin_pages.initialize(m_tabpanel);
if (m_plater) {
// load initial config
@@ -1373,10 +1364,15 @@ void MainFrame::show_device(bool should_use_native) {
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
// The legacy page is appended when printer agents are enabled. Remove that
// extra page before switching back to the normal native/legacy layout.
if (!use_printer_agents) {
if ((idx = m_tabpanel->FindPage(m_printer_view)) != wxNOT_FOUND && idx != tpMonitor) {
// The web Device page is the extra tab printer-agents mode shows alongside the native one.
// Printers that drive the native Bambu device tab have nothing to put in it, so they don't
// get it — otherwise a Bambu user sees two Device tabs, one of them permanently empty.
const bool want_web_device_tab = use_printer_agents && wxGetApp().preset_bundle != nullptr &&
!wxGetApp().preset_bundle->use_bbl_device_tab();
// Remove the extra page before switching to any layout that shouldn't have it.
if (!want_web_device_tab) {
if ((idx = m_tabpanel->FindPageByName(TAB_ID_MONITOR_WEB)) != wxNOT_FOUND) {
m_printer_view->Show(false);
m_tabpanel->RemovePage(idx);
}
@@ -1394,8 +1390,8 @@ void MainFrame::show_device(bool should_use_native) {
m_tabpanel->RemovePage(idx);
}
m_monitor->Show(false);
m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"),
std::string("tab_monitor_active"));
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
_L("Device"), "tab_monitor_active");
}
if (m_printer_view == nullptr) {
@@ -1416,28 +1412,31 @@ void MainFrame::show_device(bool should_use_native) {
// TODO: change the bitmap
if (m_tabpanel->FindPage(m_multi_machine) == wxNOT_FOUND) {
m_multi_machine->Show(false);
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
std::string("tab_multi_active"), false);
// Past the web Device tab when it is already there, so enabling multi-machine
// later can't wedge this page between the two Device tabs.
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR_WEB, TAB_ID_MONITOR}),
TAB_ID_MULTI_DEVICE, m_multi_machine, _L("Multi-device"), "tab_multi_active");
}
}
if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
}
// Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
// the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
if (m_tabpanel->FindPage(m_calibration) == wxNOT_FOUND) {
m_calibration->Show(false);
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
std::string("tab_calibration_active"), false);
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
_L("Calibration"), "tab_calibration_active");
}
if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
m_printer_view->Show(false);
m_tabpanel->AddPage(m_printer_view, _L("Device (legacy)"), std::string("tab_monitor_active"),
std::string("tab_monitor_active"), false);
} else {
m_tabpanel->SetPageText(idx, _L("Device (legacy)"));
if (want_web_device_tab) {
if ((idx = m_tabpanel->FindPage(m_printer_view)) == wxNOT_FOUND) {
m_printer_view->Show(false);
// Immediately right of the native Device tab, not at the end of the tab bar.
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MONITOR_WEB,
m_printer_view, _L("Device (Web)"), "tab_monitor_active");
} else {
m_tabpanel->SetPageText(idx, _L("Device (Web)"));
}
}
#ifdef _MSW_DARK_MODE
@@ -1445,6 +1444,7 @@ void MainFrame::show_device(bool should_use_native) {
#endif // _MSW_DARK_MODE
fit_tab_labels(); // ORCA on printer change
m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
return;
}
@@ -1466,7 +1466,8 @@ void MainFrame::show_device(bool should_use_native) {
m_monitor->SetBackgroundColour(*wxWHITE);
}
m_monitor->Show(false);
m_tabpanel->InsertPage(tpMonitor, m_monitor, _L("Device"), std::string("tab_monitor_active"), std::string("tab_monitor_active"));
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_monitor,
_L("Device"), "tab_monitor_active");
if (wxGetApp().is_enable_multi_machine()) {
if (!m_multi_machine) {
@@ -1475,18 +1476,18 @@ void MainFrame::show_device(bool should_use_native) {
}
// TODO: change the bitmap
m_multi_machine->Show(false);
m_tabpanel->InsertPage(tpMultiDevice, m_multi_machine, _L("Multi-device"), std::string("tab_multi_active"),
std::string("tab_multi_active"), false);
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_MONITOR}), TAB_ID_MULTI_DEVICE, m_multi_machine,
_L("Multi-device"), "tab_multi_active");
}
if (!m_calibration) {
m_calibration = new CalibrationPanel(m_tabpanel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_calibration->SetBackgroundColour(*wxWHITE);
}
m_calibration->Show(false);
// Calibration is always the last page, so don't use InsertPage here. Otherwise, if multi_machine page is not enabled,
// the calibration tab won't be properly added as well, due to the TabPosition::tpCalibration no longer matches the real tab position.
m_tabpanel->AddPage(m_calibration, _L("Calibration"), std::string("tab_calibration_active"),
std::string("tab_calibration_active"), false);
// Last of the built-in tabs, but plugin tabs already sit past it — anchor rather than
// append, so its position doesn't depend on the relayout() below running afterwards.
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PROJECT}), TAB_ID_CALIBRATION, m_calibration,
_L("Calibration"), "tab_calibration_active");
#ifdef _MSW_DARK_MODE
wxGetApp().UpdateDarkUIWin(this);
@@ -1519,10 +1520,17 @@ void MainFrame::show_device(bool should_use_native) {
});
}
m_printer_view->Show(false);
m_tabpanel->InsertPage(tpMonitor, m_printer_view, _L("Device"), std::string("tab_monitor_active"),
std::string("tab_monitor_active"));
m_tabpanel->InsertPage(m_tabpanel->PositionAfter({TAB_ID_PREVIEW}), TAB_ID_MONITOR, m_printer_view,
_L("Device"), "tab_monitor_active");
}
fit_tab_labels(); // ORCA on printer change
m_plugin_pages.relayout(); // re-sync plugin tabs against the native tabs just mutated above
}
bool MainFrame::is_prepare_or_preview_tab() const
{
const wxString tab = m_tabpanel->GetSelectedPageName();
return tab == TAB_ID_PREPARE || tab == TAB_ID_PREVIEW;
}
void MainFrame::fit_tab_labels()
@@ -1554,7 +1562,7 @@ void MainFrame::fit_tab_labels()
bool MainFrame::preview_only_hint()
{
if (m_plater && (m_plater->only_gcode_mode() || (m_plater->using_exported_file()))) {
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelection() %tp3DEditor;
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2% in preview mode")%m_tabpanel->GetSelectedPageName() %wxString(TAB_ID_PREPARE);
ConfirmBeforeSendDialog confirm_dlg(this, wxID_ANY, _L("Warning"));
confirm_dlg.Bind(EVT_SECONDARY_CHECK_CONFIRM, [this](wxCommandEvent& e) {
@@ -1872,22 +1880,22 @@ bool MainFrame::can_clone() const {
bool MainFrame::can_select() const
{
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
}
bool MainFrame::can_deselect() const
{
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
}
bool MainFrame::can_delete() const
{
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->is_selection_empty();
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->is_selection_empty();
}
bool MainFrame::can_delete_all() const
{
return (m_plater != nullptr) && (m_tabpanel->GetSelection() == TabPosition::tp3DEditor) && !m_plater->model().objects.empty();
return (m_plater != nullptr) && (m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE) && !m_plater->model().objects.empty();
}
bool MainFrame::can_reslice() const
@@ -1996,7 +2004,7 @@ wxBoxSizer* MainFrame::create_side_tools()
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else
wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
this->m_tabpanel->SetSelection(tpPreview);
this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
});
@@ -3143,7 +3151,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().app_config->set_bool("auto_perspective", !wxGetApp().app_config->get_bool("auto_perspective"));
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().app_config->get_bool("auto_perspective"); }, this);
viewMenu->AppendSeparator();
@@ -3152,7 +3160,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_gcode_window();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this, [this]() { return m_tabpanel->GetSelection() == tpPreview; },
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW; },
[this]() { return wxGetApp().show_gcode_window(); }, this);
append_menu_check_item(
@@ -3161,7 +3169,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_3d_navigator();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
this, [this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_3d_navigator(); }, this);
append_menu_check_item(viewMenu, wxID_ANY, _L("Show Gridlines"), _L("Show Gridlines on plate"),
@@ -3169,15 +3177,14 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_plate_gridlines();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
}, this,
[this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; },
[this]() { return is_prepare_or_preview_tab(); },
[this]() { return wxGetApp().show_plate_gridlines(); }, this);
append_menu_item(
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
[this]() {
return (m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview) &&
m_plater->is_sidebar_enabled();
return is_prepare_or_preview_tab() && m_plater->is_sidebar_enabled();
},
this);
@@ -3199,7 +3206,7 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().toggle_show_outline();
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor; },
this, [this]() { return m_tabpanel->GetSelectedPageName() == TAB_ID_PREPARE; },
[this]() { return wxGetApp().show_outline(); }, this);
/*viewMenu->AppendSeparator();
@@ -4000,13 +4007,16 @@ void MainFrame::select_tab(wxPanel* panel)
wxGetApp().params_dialog()->Popup();
return;
}
// Not panel->GetName(): Prepare and Preview share the single m_plater window, so the
// window has no one correct name. The slot -> id lookup is the only correct resolution.
int page_idx = m_tabpanel->FindPage(panel);
if (page_idx == tp3DEditor && m_tabpanel->GetSelection() == tpPreview)
wxString page_name = (page_idx == wxNOT_FOUND) ? wxString() : m_tabpanel->GetPageName(static_cast<size_t>(page_idx));
if (page_name == TAB_ID_PREPARE && m_tabpanel->GetSelectedPageName() == TAB_ID_PREVIEW)
return;
//BBS GUI refactor: remove unused layout new/dlg
/*if (page_idx != wxNOT_FOUND && m_layout == ESettingsLayout::Dlg)
page_idx++;*/
select_tab(size_t(page_idx));
select_tab(page_name);
}
//BBS
@@ -4014,7 +4024,7 @@ void MainFrame::jump_to_monitor(std::string dev_id)
{
if(!m_monitor)
return;
m_tabpanel->SetSelection(tpMonitor);
m_tabpanel->SelectPageByName(TAB_ID_MONITOR);
if (!dev_id.empty()) {
((MonitorPanel*)m_monitor)->select_machine(dev_id);
}
@@ -4024,26 +4034,26 @@ void MainFrame::jump_to_multipage()
{
if(!m_multi_machine)
return;
m_tabpanel->SetSelection(tpMultiDevice);
m_tabpanel->SelectPageByName(TAB_ID_MULTI_DEVICE);
((MultiMachinePage*)m_multi_machine)->jump_to_send_page();
}
//BBS GUI refactor: remove unused layout new/dlg
void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
void MainFrame::select_tab(const wxString& id/* = wxString()*/)
{
//bool tabpanel_was_hidden = false;
// Controls on page are created on active page of active tab now.
// We should select/activate tab before its showing to avoid an UI-flickering
auto select = [this, tab](bool was_hidden) {
// when tab == -1, it means we should show the last selected tab
auto select = [this, id](bool was_hidden) {
// when id is empty, it means we should show the last selected tab
//BBS GUI refactor: remove unused layout new/dlg
//size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : (m_layout == ESettingsLayout::Dlg && tab != 0) ? tab - 1 : tab;
size_t new_selection = tab == (size_t)(-1) ? m_last_selected_tab : tab;
wxString new_selection = id.empty() ? m_last_selected_tab : id;
if (m_tabpanel->GetSelection() != (int)new_selection)
m_tabpanel->SetSelection(new_selection);
if (m_tabpanel->GetSelectedPageName() != new_selection)
m_tabpanel->SelectPageByName(new_selection);
#ifdef _MSW_DARK_MODE
/*if (wxGetApp().tabs_as_menu()) {
if (Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection)))
@@ -4052,10 +4062,12 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
m_plater->get_current_canvas3D()->render();
}*/
#endif
if (tab == MainFrame::tp3DEditor && m_layout == ESettingsLayout::Old)
// Intentionally `id`, not `new_selection`: the fallback-to-last-tab path must not
// trigger this render even when the last selected tab was Prepare.
if (id == TAB_ID_PREPARE && m_layout == ESettingsLayout::Old)
m_plater->canvas3D()->render();
else if (was_hidden) {
Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPage(new_selection));
Tab* cur_tab = dynamic_cast<Tab*>(m_tabpanel->GetPageByName(new_selection));
if (cur_tab)
cur_tab->OnActivate();
}
@@ -4064,10 +4076,10 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
select(false);
}
void MainFrame::request_select_tab(TabPosition pos)
void MainFrame::request_select_tab(const wxString& id)
{
wxCommandEvent* evt = new wxCommandEvent(EVT_SELECT_TAB);
evt->SetInt(pos);
evt->SetString(id);
wxQueueEvent(this, evt);
}
@@ -4333,21 +4345,33 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
void MainFrame::load_printer_url()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_device_tab() || wxGetApp().app_config->get_bool("use_printer_agents"))
if (preset_bundle.use_bbl_device_tab() && !wxGetApp().app_config->get_bool("use_printer_agents"))
return;
auto cfg = preset_bundle.printers.get_edited_preset().config;
if (cfg.opt_string("print_host").empty()) {
if (auto *device_manager = wxGetApp().getDeviceManager()) {
auto *machine = device_manager->get_selected_machine();
if (!machine) {
auto machines = device_manager->get_my_machine_list();
if (machines.size() == 1)
machine = machines.begin()->second;
}
if (machine && !machine->get_dev_ip().empty())
cfg.opt_string("print_host") = machine->get_dev_ip();
}
}
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
wxString apikey;
const auto host_type = cfg.option<ConfigOptionEnum<PrintHostType>>("host_type")->value;
if (cfg.has("printhost_apikey") && (host_type == htPrusaLink || host_type == htPrusaConnect))
if (cfg.has("printhost_apikey") && host_type != htSimplyPrint)
apikey = cfg.opt_string("printhost_apikey");
if (!url.empty()) {
load_printer_url(url, apikey);
}
}
bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelection() == TabPosition::tpMonitor; }
bool MainFrame::is_printer_view() const { return m_tabpanel->GetSelectedPageName() == TAB_ID_MONITOR; }
void MainFrame::refresh_plugin_tips()
+23 -17
View File
@@ -35,6 +35,21 @@
#include "PrinterWebView.hpp"
#include "calib_dlg.hpp"
#include "MultiMachinePage.hpp"
#include "slic3r/plugin/host/PluginPages.hpp"
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
// names rather than positional indices so optional pages cannot shift them.
#define TAB_ID_HOME "home"
#define TAB_ID_PREPARE "prepare"
#define TAB_ID_PREVIEW "preview"
#define TAB_ID_MONITOR "monitor"
// Printer-agents mode shows the legacy web page alongside the native Device tab, so it needs an
// id of its own: sharing TAB_ID_MONITOR makes every name lookup resolve to whichever of the two
// comes first, which silently defeats PluginPages' selection round-trip across a tab relayout.
#define TAB_ID_MONITOR_WEB "monitor_web"
#define TAB_ID_MULTI_DEVICE "multi_device"
#define TAB_ID_PROJECT "project"
#define TAB_ID_CALIBRATION "calibration"
#define ENABEL_PRINT_ALL 0
@@ -115,7 +130,7 @@ class MainFrame : public DPIFrame
wxMenuItem* m_menu_item_reslice_now { nullptr };
wxSizer* m_main_sizer{ nullptr };
size_t m_last_selected_tab;
wxString m_last_selected_tab;
std::string get_base_name(const wxString &full_name, const char *extension = nullptr) const;
std::string get_dir_name(const wxString &full_name) const;
@@ -214,19 +229,6 @@ public:
#ifdef __APPLE__
bool get_mac_full_screen() { return m_mac_fullscreen; }
#endif
//BBS GUI refactor
enum TabPosition
{
tpHome = 0,
tp3DEditor = 1,
tpPreview = 2,
tpMonitor = 3,
tpMultiDevice = 4,
tpProject = 5,
tpCalibration = 6,
tpAuxiliary = 7,
toDebugTool = 8,
};
//BBS: add slice&&print status update logic
enum SlicePrintEventType
@@ -326,8 +328,8 @@ public:
// When tab == -1, will be selected last selected tab
//BBS: GUI refactor
void select_tab(wxPanel* panel);
void select_tab(size_t tab = size_t(-1));
void request_select_tab(TabPosition pos);
void select_tab(const wxString& id = wxString());
void request_select_tab(const wxString& id);
int get_calibration_curr_tab();
void select_view(const std::string& direction);
// Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig
@@ -360,6 +362,9 @@ public:
//SoftFever
void show_device(bool should_use_native);
void fit_tab_labels(); // ORCA
// True while either of the two tabs backed by m_plater is selected.
bool is_prepare_or_preview_tab() const;
PluginPages& plugin_pages() { return m_plugin_pages; }
PA_Calibration_Dlg* m_pa_calib_dlg{ nullptr };
FlowRateCalibrationDialog* m_flow_rate_calib_dlg{ nullptr };
@@ -385,7 +390,8 @@ public:
CalibrationPanel* m_calibration{ nullptr };
WebViewPanel* m_webview { nullptr };
PrinterWebView* m_printer_view{nullptr};
wxLogWindow* m_log_window { nullptr };
PluginPages m_plugin_pages;
wxLogWindow* m_log_window { nullptr };
// BBS
//wxBookCtrlBase* m_tabpanel { nullptr };
Notebook* m_tabpanel{ nullptr };
+9 -6
View File
@@ -186,17 +186,17 @@ void MonitorPanel::init_tabpanel()
//m_status_add_machine_panel = new AddMachinePanel(m_tabpanel);
m_status_info_panel = new StatusPanel(m_tabpanel);
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), "", true);
m_tabpanel->AddPage(m_status_info_panel, _L("Status"), true);
m_media_file_panel = new MediaFilePanel(m_tabpanel);
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), "", false);
//m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), "", false);
m_tabpanel->AddPage(m_media_file_panel, _L("Storage"), false);
//m_tabpanel->AddPage(m_media_file_panel, _L("Internal Storage"), false);
m_upgrade_panel = new UpgradePanel(m_tabpanel);
m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), "", false);
m_tabpanel->AddPage(m_upgrade_panel, _L_CONTEXT(L_CONTEXT("Update", "Firmware"), "Firmware"), false);
m_hms_panel = new HMSPanel(m_tabpanel);
m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), "", false);
m_tabpanel->AddPage(m_hms_panel, _L("Assistant(HMS)"), false);
std::string network_ver = Slic3r::NetworkAgent::get_version();
if (!network_ver.empty()) {
@@ -413,7 +413,10 @@ void MonitorPanel::update_hms_tag()
bool MonitorPanel::Show(bool show)
{
#ifdef __APPLE__
wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
// Notebook::InsertPage() hides every page it appends, so this also runs while MainFrame is
// still constructing, before GUI_App::mainframe is assigned. Same guard as Plater::Show().
if (wxGetApp().mainframe)
wxGetApp().mainframe->SetMinSize(wxGetApp().plater()->GetMinSize());
#endif
NetworkAgent* m_agent = wxGetApp().getAgent();
+3 -3
View File
@@ -86,9 +86,9 @@ void MultiMachinePage::init_tabpanel()
m_cloud_task_manager = new CloudTaskManagerPage(m_tabpanel);
m_machine_manager = new MultiMachineManagerPage(m_tabpanel);
m_tabpanel->AddPage(m_machine_manager, _L("Device"), "", true);
m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), "", false);
m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), "", false);
m_tabpanel->AddPage(m_machine_manager, _L("Device"), true);
m_tabpanel->AddPage(m_local_task_manager, _L("Task Sending"), false);
m_tabpanel->AddPage(m_cloud_task_manager, _L("Task Sent"), false);
}
void MultiMachinePage::init_timer()
+41 -7
View File
@@ -120,11 +120,11 @@ void ButtonsListCtrl::Rescale()
void ButtonsListCtrl::SetSelection(int sel)
{
if (m_selection == sel)
if (m_selection == sel && sel >= 0 && sel < static_cast<int>(m_pageButtons.size()))
return;
// BBS: change button color
wxColour selected_btn_bg("#009688"); // Gradient #009688
if (m_selection >= 0) {
if (m_selection >= 0 && m_selection < static_cast<int>(m_pageButtons.size())) {
StateColor bg_color = StateColor(
std::pair{wxColour(107, 107, 107), (int) StateColor::Hovered},
std::pair{wxColour(59, 68, 70), (int) StateColor::Normal});
@@ -132,9 +132,15 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal}
);
m_pageButtons[m_selection]->SetSelected(false);
m_pageButtons[m_selection]->SetTextColor(text_color);
}
if (sel < 0 || sel >= static_cast<int>(m_pageButtons.size())) {
m_selection = -1;
Refresh();
return;
}
m_selection = sel;
StateColor bg_color = StateColor(
@@ -145,17 +151,19 @@ void ButtonsListCtrl::SetSelection(int sel)
StateColor text_color = StateColor(
std::pair{wxColour(254, 254, 254), (int) StateColor::Normal}
);
m_pageButtons[m_selection]->SetSelected(true);
m_pageButtons[m_selection]->SetTextColor(text_color);
Refresh();
}
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const std::string &inactive_bmp_name)
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
{
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
btn->SetCornerRadius(0);
if (bmp_name.empty() && bmp.IsOk())
btn->SetIcon(bmp);
int em = em_unit(this);
//BBS set size for button
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
@@ -168,8 +176,6 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
StateColor text_color = StateColor(
std::pair{wxColour(254,254, 254), (int) StateColor::Normal});
btn->SetTextColor(text_color);
btn->SetInactiveIcon(inactive_bmp_name);
btn->SetSelected(false);
btn->Bind(wxEVT_BUTTON, [this, btn](wxCommandEvent& event) {
if (auto it = std::find(m_pageButtons.begin(), m_pageButtons.end(), btn); it != m_pageButtons.end()) {
auto sel = it - m_pageButtons.begin();
@@ -192,6 +198,14 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
void ButtonsListCtrl::RemovePage(size_t n)
{
if (n >= m_pageButtons.size())
return;
if (m_selection == static_cast<int>(n))
m_selection = -1;
else if (m_selection > static_cast<int>(n))
--m_selection;
Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
@@ -240,6 +254,24 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel();
}
// ORCA
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
{
if (m_overflow_button == button)
return;
if (m_overflow_button != nullptr)
m_sizer->Detach(m_overflow_button);
m_overflow_button = button;
if (m_overflow_button != nullptr)
// Right after the tab buttons (index 0), ahead of any stretch spacer / side_tools.
m_sizer->Insert(1, m_overflow_button, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxBOTTOM, m_btn_margin);
m_sizer->Layout();
}
//#endif // _WIN32
void Notebook::Init()
@@ -253,6 +285,8 @@ void Notebook::Init()
m_showTimeout = m_hideTimeout = 0;
m_pageNames.clear();
/* On Linux, Gstreamer wxMediaCtrl does not seem to get along well with
* 32-bit X11 visuals (the overlay does not work). Is this a wxWindows
* bug? Is this a Gstreamer bug? No idea, but it is our problem ...
+106 -33
View File
@@ -3,7 +3,11 @@
//#ifdef _WIN32
#include <initializer_list>
#include <string>
#include <vector>
#include <wx/bookctrl.h>
#include <wx/bitmap.h>
#include <wx/sizer.h>
class ScalableButton;
@@ -23,13 +27,16 @@ public:
void SetSelection(int sel);
void UpdateMode();
void Rescale();
bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const std::string &inactive_bmp_name = "");
bool InsertPage(size_t n, const wxString &text, bool bSelect = false, const std::string &bmp_name = "", const wxBitmap &bmp = wxNullBitmap);
void RemovePage(size_t n);
bool SetPageImage(size_t n, const std::string& bmp_name) const;
void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const;
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
// 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);
private:
wxFlexGridSizer* m_buttons_sizer;
@@ -40,9 +47,10 @@ private:
int m_btn_margin;
int m_line_margin;
std::vector<wxString> m_pageLabels; // ORCA
wxWindow* m_overflow_button{nullptr}; // ORCA
};
class Notebook: public wxBookCtrlBase
class Notebook : public wxBookCtrlBase
{
public:
Notebook(wxWindow * parent,
@@ -103,7 +111,7 @@ public:
// by this control) and show it immediately.
bool ShowNewPage(wxWindow * page)
{
return AddPage(page, wxString(), "", "");
return AddPage(page, wxString(), false, NO_IMAGE);
}
@@ -135,51 +143,56 @@ public:
// Implement base class pure virtual methods.
// adds a new page to the control
bool AddPage(wxWindow* page,
// Page management. Every insertion funnels through the InsertPage() below; `id` is the
// stable page name FindPageByName() resolves. Built-in tabs name a resource bitmap,
// plugin pages hand over a ready wxBitmap; wx's own imageId overloads carry neither.
bool AddPage(const wxString& id,
wxWindow* page,
const wxString& text,
const std::string& bmp_name,
const std::string& inactive_bmp_name,
const std::string& bmp_name = "",
bool bSelect = false)
{
DoInvalidateBestSize();
return InsertPage(GetPageCount(), page, text, bmp_name, inactive_bmp_name, bSelect);
return InsertPage(GetPageCount(), id, page, text, bmp_name, bSelect);
}
// Page management
virtual bool InsertPage(size_t n,
wxWindow * page,
const wxString & text,
bool bSelect = false,
int imageId = NO_IMAGE) override
bool AddPage(wxWindow* page, const wxString& text, bool bSelect = false, int imageId = NO_IMAGE) override
{
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect, imageId))
DoInvalidateBestSize();
return InsertPage(GetPageCount(), page, text, bSelect, imageId);
}
bool InsertPage(size_t n,
const wxString& id,
wxWindow * page,
const wxString & text,
const std::string& bmp_name = "",
bool bSelect = false,
const wxBitmap& bmp = wxNullBitmap)
{
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
return false;
GetBtnsListCtrl()->InsertPage(n, text, bSelect);
m_pageNames.insert(m_pageNames.begin() + n, id);
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, bmp);
// wxBookCtrlBase::InsertPage() only inserts into the page list and sizes the new
// page to the current page's rect — it never touches visibility, and a freshly
// constructed page defaults to shown. Without this it renders on top of whatever
// page is currently selected until the next SetSelection() call hides it.
if (!DoSetSelectionAfterInsertion(n, bSelect))
page->Hide();
return true;
}
bool InsertPage(size_t n,
wxWindow * page,
const wxString & text,
const std::string& bmp_name = "",
const std::string& inactive_bmp_name = "",
bool bSelect = false)
virtual bool InsertPage(size_t n,
wxWindow * page,
const wxString & text,
bool bSelect = false,
int WXUNUSED(imageId) = NO_IMAGE) override
{
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
return false;
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name, inactive_bmp_name);
if (bSelect)
SetSelection(n);
return true;
return InsertPage(n, wxString(), page, text, "", bSelect);
}
virtual int SetSelection(size_t n) override
@@ -211,8 +224,8 @@ public:
return DoSetSelection(n);
}
// Neither labels nor images are supported but we still store the labels
// just in case the user code attaches some importance to them.
// Labels are stored by the custom button list; wx's image-list API is unused — tab icons
// are set directly on the buttons, either from a resource name or a ready wxBitmap.
virtual bool SetPageText(size_t n, const wxString & strText) override
{
wxCHECK_MSG(n < GetPageCount(), false, wxS("Invalid page"));
@@ -251,7 +264,64 @@ public:
page->SetFocus();
}
// The base clears its page list directly instead of calling DoRemovePage() per page,
// which would leave m_pageNames behind. No caller today; kept in sync regardless.
virtual bool DeleteAllPages() override
{
m_pageNames.clear();
return wxBookCtrlBase::DeleteAllPages();
}
ButtonsListCtrl* GetBtnsListCtrl() const { return static_cast<ButtonsListCtrl*>(m_bookctrl); }
void SetOverflowButton(wxWindow* button) { GetBtnsListCtrl()->SetOverflowButton(button); }
// Insertion index just past the first of `ids` that is present, or the end of the bar
// if none is — lets call sites state tab order as "after X" instead of re-deriving it.
size_t PositionAfter(std::initializer_list<const char*> ids) const
{
for (const char* id : ids)
if (const int idx = FindPageByName(id); idx != wxNOT_FOUND)
return static_cast<size_t>(idx) + 1;
return GetPageCount();
}
int FindPageByName(const wxString& id) const
{
if (id.empty())
return wxNOT_FOUND;
for (size_t i = 0; i < m_pageNames.size(); ++i)
if (m_pageNames[i] == id)
return static_cast<int>(i);
return wxNOT_FOUND;
}
wxWindow* GetPageByName(const wxString& id) const
{
const int idx = FindPageByName(id);
return idx == wxNOT_FOUND ? nullptr : GetPage(static_cast<size_t>(idx));
}
bool SelectPageByName(const wxString& id)
{
const int idx = FindPageByName(id);
if (idx == wxNOT_FOUND)
return false;
SetSelection(static_cast<size_t>(idx));
return true;
}
// Inverse of FindPageByName: index -> id. Empty string for an out-of-range
// index or a page that was never given an id (e.g. settings Tab pages).
wxString GetPageName(size_t n) const
{
return n < m_pageNames.size() ? m_pageNames[n] : wxString();
}
wxString GetSelectedPageName() const
{
const int sel = GetSelection();
return sel < 0 ? wxString() : GetPageName(static_cast<size_t>(sel));
}
void UpdateMode()
{
@@ -369,6 +439,7 @@ protected:
wxWindow* const win = wxBookCtrlBase::DoRemovePage(page);
if (win)
{
m_pageNames.erase(m_pageNames.begin() + page);
GetBtnsListCtrl()->RemovePage(page);
DoSetSelectionAfterRemoval(page);
}
@@ -394,6 +465,8 @@ protected:
private:
void Init();
std::vector<wxString> m_pageNames; // index-parallel to wxBookCtrlBase::m_pages
wxShowEffect m_showEffect,
m_hideEffect;
+6 -6
View File
@@ -1918,7 +1918,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L"");
}
else {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
return false;
};
@@ -1985,7 +1985,7 @@ void NotificationManager::push_validate_error_notification(StringObjectException
wxGetApp().sidebar().jump_to_option(opt, opt_type, L"");
}
else {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
return false;
};
@@ -2015,7 +2015,7 @@ void NotificationManager::push_slicing_error_notification(const std::string &tex
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
}
if (!ovs.empty()) {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items(ovs);
}
return false;
@@ -2046,7 +2046,7 @@ void NotificationManager::push_slicing_warning_notification(const std::string& t
auto& objects = wxGetApp().model().objects;
auto iter = std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; });
if (iter != objects.end()) {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items({ {*iter, nullptr} });
}
return false;
@@ -2693,7 +2693,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
if (iter != objects.end()) { ovs.push_back({ *iter, nullptr }); }
}
if (!ovs.empty()) {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items(ovs);
wxGetApp().obj_list()->update_selections_on_canvas();
}
@@ -2777,7 +2777,7 @@ void NotificationManager::push_slicing_serious_warning_notification(const std::s
}
}
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (!sel_items.empty()) {
obj_list->select_items(sel_items);
+52 -34
View File
@@ -3287,7 +3287,9 @@ void Sidebar::update_all_preset_comboboxes()
: MainFrame::PrintSelectType::eSendGcode;
}
if (!use_native_device_tab || use_printer_agents)
if (use_printer_agents)
p_mainframe->load_printer_url();
else if (!use_native_device_tab)
p_mainframe->load_printer_url(url, apikey);
@@ -5781,6 +5783,8 @@ private:
bool show_warning_dialog { false };
};
Plater::~Plater() = default;
const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf)", std::regex::icase);
const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase);
const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase);
@@ -5795,7 +5799,7 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
#endif // WIN32
m_mainframe.Raise();
m_mainframe.select_tab(size_t(MainFrame::tp3DEditor));
m_mainframe.select_tab(TAB_ID_PREPARE);
if (wxGetApp().is_editor())
m_plater.select_view_3D("3D");
@@ -6577,9 +6581,9 @@ void Plater::priv::select_next_view_3D()
{
if (current_panel == view3D)
wxGetApp().mainframe->select_tab(size_t(MainFrame::tpPreview));
wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
else if (current_panel == preview)
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
// else if (current_panel == assemble_view)
// set_current_panel(view3D);
}
@@ -7878,7 +7882,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
q->select_plate(first_plate_index);
//set to 3d tab
q->select_view_3D("Preview");
wxGetApp().mainframe->select_tab(MainFrame::tpPreview);
wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
}
else {
//set to 3d tab
@@ -7897,7 +7901,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
else {
//always set to 3D after loading files
q->select_view_3D("3D");
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
if (load_model) {
@@ -8805,7 +8809,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni
}
}
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (inst_idx != -1) {
auto* model = wxGetApp().obj_list()->GetModel();
@@ -8834,7 +8838,7 @@ void Plater::priv::process_validation_warning(StringObjectException const &warni
} else {
auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end();
if (iter != objects.end()) {
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
wxGetApp().obj_list()->select_items({{*iter, nullptr}});
wxGetApp().obj_list()->update_selections_on_canvas();
}
@@ -11218,13 +11222,19 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
}
const int new_sel = e.GetSelection();
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
if (new_sel == wxNOT_FOUND) {
// GetPage(new_sel) below needs a valid index.
e.Skip();
return;
}
const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel);
sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW;
update_sidebar();
int old_sel = e.GetOldSelection();
const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents");
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
if (use_native_device_tab && new_name == TAB_ID_MONITOR) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!use_printer_agents && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
@@ -11236,9 +11246,17 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
}
}
} else {
if (new_sel == MainFrame::tpMonitor && wxGetApp().preset_bundle != nullptr) {
// Pointer test, not a name lookup: in printer-agents mode this page is TAB_ID_MONITOR_WEB
// while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds
// TAB_ID_MONITOR itself.
const bool selecting_web_device_tab = main_frame->m_printer_view &&
main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view;
if (selecting_web_device_tab) {
// Use the selected discovered machine when the preset has no host.
main_frame->load_printer_url();
} else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) {
auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
wxString url = cfg.opt_string("print_host_webui").empty() ? cfg.opt_string("print_host") : cfg.opt_string("print_host_webui");
wxString url = from_u8(PrintHost::get_print_host_webui(&cfg));
if (main_frame->m_printer_view && url.empty()) {
// It's missing_connection page, reload so that we can replay the gif image
main_frame->m_printer_view->reload();
@@ -12099,7 +12117,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all)
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_ALL));
else
wxPostEvent(q, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE));
wxGetApp().mainframe->m_tabpanel->SetSelection(MainFrame::TabPosition::tpPreview);
wxGetApp().mainframe->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW);
}
return false;
}
@@ -12667,7 +12685,7 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed
ModelWipeTower& tower = model.wipe_tower;
tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx));
tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle");
tower.rotation = config.opt_float("wipe_tower_rotation_angle");
}
}
const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager();
@@ -12777,7 +12795,7 @@ void Plater::priv::undo_redo_to(std::vector<UndoRedo::Snapshot>::const_iterator
ModelWipeTower& tower = model.wipe_tower;
tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx));
tower.rotation = proj_cfg.opt_float("wipe_tower_rotation_angle");
tower.rotation = config.opt_float("wipe_tower_rotation_angle");
}
}
const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx;
@@ -13056,7 +13074,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_
get_notification_manager()->clear_all();
if (!silent)
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
//get_partplate_list().reinit();
//get_partplate_list().update_slice_context_to_current_plate(p->background_process);
@@ -13205,7 +13223,7 @@ void Plater::load_project(wxString const& filename2,
if (!m_exported_file) {
p->select_view("topfront");
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
}
else {
p->partplate_list.select_plate_view();
@@ -13319,7 +13337,7 @@ void Plater::import_model_id(wxString download_info)
const int max_retries = 3;
/* jump to 3D eidtor */
wxGetApp().mainframe->select_tab((size_t)MainFrame::TabPosition::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
/* prepare progress dialog */
bool cont = true;
@@ -13628,7 +13646,7 @@ void Plater::calib_pa(const Calib_Params& params)
{
const auto calib_pa_name = wxString::Format(L"Pressure Advance Test");
new_project(false, false, calib_pa_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
@@ -14109,7 +14127,7 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) {
if (new_project(false, false, calib_name) == wxID_CANCEL)
return;
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (is_linear) {
if (pass == 1)
@@ -14146,7 +14164,7 @@ void Plater::calib_temp(const Calib_Params& params) {
const auto calib_temp_name = wxString::Format(L"Nozzle temperature test");
new_project(false, false, calib_temp_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Temp_Tower) return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc"))
@@ -14226,7 +14244,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
{
const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test");
new_project(false, false, calib_vol_speed_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Vol_speed_Tower)
return;
if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc"))
@@ -14305,7 +14323,7 @@ void Plater::calib_retraction(const Calib_Params& params)
{
const auto calib_retraction_name = wxString::Format(L"Retraction");
new_project(false, false, calib_retraction_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Retraction_tower)
return;
@@ -14365,7 +14383,7 @@ void Plater::calib_VFA(const Calib_Params& params)
{
const auto calib_vfa_name = wxString::Format(L"VFA test");
new_project(false, false, calib_vfa_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_VFA_Tower)
return;
@@ -14448,7 +14466,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Frequency test");
new_project(false, false, calib_input_shaping_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_freq)
return;
@@ -14514,7 +14532,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
{
const auto calib_input_shaping_name = wxString::Format(L"Input shaping Damping test");
new_project(false, false, calib_input_shaping_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Input_shaping_damp)
return;
@@ -14579,7 +14597,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
{
const auto Calib_Cornering = wxString::Format(L"Cornering test");
new_project(false, false, Calib_Cornering);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
if (params.mode != CalibMode::Calib_Cornering)
return;
@@ -14712,7 +14730,7 @@ void Plater::load_gcode(const wxString& filename)
//p->gcode_result.reset();
//reset_gcode_toolpaths();
p->preview->reload_print(m_only_gcode);
wxGetApp().mainframe->select_tab(MainFrame::tpPreview);
wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
p->set_current_panel(p->preview, true);
p->get_current_canvas3D()->render();
//p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file.")));
@@ -15385,7 +15403,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting)
wxGetApp().app_config->set("import_project_action", std::to_string(choice));
// BBS: jump to plater panel
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
return load_type;
}
@@ -15614,7 +15632,7 @@ void Plater::reset_with_confirm()
.ShowModal() == wxID_YES) {
reset();
// BBS: jump to plater panel
wxGetApp().mainframe->select_tab(size_t(0));
wxGetApp().mainframe->select_tab(TAB_ID_HOME);
}
}
@@ -17428,7 +17446,7 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn)
//BBS
void Plater::send_calibration_job_finished(wxCommandEvent & evt)
{
p->main_frame->request_select_tab(MainFrame::TabPosition::tpCalibration);
p->main_frame->request_select_tab(TAB_ID_CALIBRATION);
auto calibration_panel = p->main_frame->m_calibration;
if (calibration_panel) {
auto curr_wizard = static_cast<CalibrationWizard*>(calibration_panel->get_tabpanel()->GetPage(evt.GetInt()));
@@ -17460,7 +17478,7 @@ void Plater::print_job_finished(wxCommandEvent &evt)
if (!dev) return;
dev->set_selected_machine(evt.GetString().ToStdString());
p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor);
p->main_frame->request_select_tab(TAB_ID_MONITOR);
//jump to monitor and select device status panel
MonitorPanel* curr_monitor = p->main_frame->m_monitor;
if(curr_monitor)
@@ -17475,7 +17493,7 @@ void Plater::send_job_finished(wxCommandEvent& evt)
send_gcode_finish(evt.GetString());
p->hide_send_to_printer_dlg();
//p->main_frame->request_select_tab(MainFrame::TabPosition::tpMonitor);
//p->main_frame->request_select_tab(TAB_ID_MONITOR);
////jump to monitor and select device status panel
//MonitorPanel* curr_monitor = p->main_frame->m_monitor;
//if (curr_monitor)
@@ -18369,7 +18387,7 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar
MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page"));
auto result = dlg.ShowModal();
if (result == wxFORWARD) {
wxGetApp().mainframe->select_tab(size_t(MainFrame::tpMonitor));
wxGetApp().mainframe->select_tab(TAB_ID_MONITOR);
}
}
+2 -2
View File
@@ -290,7 +290,7 @@ public:
Plater(const Plater &) = delete;
Plater &operator=(Plater &&) = delete;
Plater &operator=(const Plater &) = delete;
~Plater() = default;
~Plater();
bool Show(bool show = true);
@@ -1020,4 +1020,4 @@ wxArrayString get_all_camera_view_type();
} // namespace GUI
} // namespace Slic3r
#endif
#endif
+1 -34
View File
@@ -15,39 +15,6 @@ namespace Slic3r { namespace GUI {
namespace {
// Low-specificity element defaults (no !important) for UNSTYLED plugin HTML, so a bare
// plugin page looks native while any CSS the plugin ships still wins. Built on the
// --orca-* variables the host injects (see WebViewHostDialog); document-start injected
// AFTER the host contract so the variables are defined (shares the base injector's
// WebView2 timing guard).
std::string plugin_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
@@ -129,7 +96,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent,
void PluginWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(plugin_defaults_user_script()));
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
}
}
+16 -1
View File
@@ -1748,11 +1748,26 @@ void PreferencesDialog::create_items()
g_sizer->Add(item_pop_up_filament_map_dialog);
#endif
//// GENERAL > Plugins
g_sizer->Add(create_item_title(_L("Plugins")), 1, wxEXPAND);
auto item_plugin_pages_visible_count = create_item_spinctrl(
_L("Visible plugin pages"),
"",
_L("pages"),
_L("Number of plugin pages shown as fixed tabs before the remaining pages collapse into a dropdown on the last tab."),
SETTING_PLUGIN_PAGES_VISIBLE_COUNT,
PLUGIN_PAGES_VISIBLE_COUNT_MIN,
PLUGIN_PAGES_VISIBLE_COUNT_MAX,
[](int value) { wxGetApp().mainframe->plugin_pages().set_visible_page_count(value); }
);
g_sizer->Add(item_plugin_pages_visible_count);
g_sizer->AddSpacer(FromDIP(10));
sizer_page->Add(g_sizer, 0, wxEXPAND);
//////////////////////////
//// CONTROL TAB
//// CONTROL TAB
/////////////////////////////////////
m_pref_tabs->AppendItem(_L("Control"));
f_sizers.push_back(new wxFlexGridSizer(1, 1, v_gap, 0));
+1 -1
View File
@@ -1049,7 +1049,7 @@ bool PlaterPresetComboBox::switch_to_tab()
//BBS Select NoteBook Tab params
if (tab->GetParent() == wxGetApp().params_panel())
wxGetApp().mainframe->select_tab(MainFrame::tp3DEditor);
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE);
else {
wxGetApp().params_dialog()->Popup();
tab->OnActivate();
+1 -1
View File
@@ -39,7 +39,7 @@ public:
PresetComboBox(wxWindow* parent, Preset::Type preset_type, const wxSize& size = wxDefaultSize, PresetBundle* preset_bundle = nullptr);
~PresetComboBox();
enum LabelItemType {
enum LabelItemType : std::size_t {
LABEL_ITEM_PHYSICAL_PRINTER = 0xffffff01,
LABEL_ITEM_PRINTER_MODELS,
LABEL_ITEM_DISABLED,
+51 -10
View File
@@ -74,7 +74,18 @@ ProjectPanel::ProjectPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
Fit();
}
ProjectPanel::~ProjectPanel() {}
ProjectPanel::~ProjectPanel()
{
shutdown();
}
void ProjectPanel::shutdown()
{
m_reload_cancel_token->store(true, std::memory_order_release);
if (m_reload_task && m_reload_task->joinable())
m_reload_task->join();
m_reload_task.reset();
}
// Helper to convert newlines to <br>
static std::string convert_newlines_to_br(const std::string& text) {
@@ -101,7 +112,17 @@ void ProjectPanel::onWebNavigating(wxWebViewEvent& evt)
void ProjectPanel::on_reload(wxCommandEvent& evt)
{
boost::thread reload = boost::thread([this] {
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
m_reload_cancel_token->load(std::memory_order_acquire))
return;
if (m_reload_task && m_reload_task->joinable())
m_reload_task->join();
const auto cancel_token = m_reload_cancel_token;
m_reload_task = std::make_unique<boost::thread>([this, cancel_token] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return;
std::string update_type;
std::string license;
std::string model_name;
@@ -115,6 +136,9 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
std::map<std::string, std::vector<json>> files;
if (wxGetApp().plater() == nullptr)
return;
Model model = wxGetApp().plater()->model();
auto model_info = model.model_info;
@@ -156,7 +180,14 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
std::string file_path = encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str());
if (!file_path.empty()) {
files = Reload(file_path);
wxGetApp().CallAfter([this, file_path, files] { m_auxiliary->Reload(file_path, files); });
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return;
wxGetApp().CallAfter([this, cancel_token, file_path, files] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return;
m_auxiliary->Reload(file_path, files);
});
} else {
clear_model_info();
return;
@@ -215,15 +246,18 @@ void ProjectPanel::on_reload(wxCommandEvent& evt)
json m_Res = json::object();
m_Res["command"] = "show_3mf_info";
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
m_Res["model"] = j;
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
if (m_web_init_completed) {
wxGetApp().CallAfter([this, strJS] {
if (m_web_init_completed.load(std::memory_order_acquire) &&
!cancel_token->load(std::memory_order_acquire) && wxTheApp != nullptr && !wxGetApp().is_closing()) {
wxGetApp().CallAfter([this, cancel_token, strJS] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return;
RunScript(strJS.ToStdString());
});
});
}
});
}
@@ -264,7 +298,7 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
}
}
else if (strCmd == "request_3mf_info") {
m_web_init_completed = true;
m_web_init_completed.store(true, std::memory_order_release);
}
else if (strCmd == "edit_project_info") {
show_info_editor(true);
@@ -307,13 +341,20 @@ void ProjectPanel::update_model_data()
void ProjectPanel::clear_model_info()
{
if (wxTheApp == nullptr || wxGetApp().is_closing() ||
m_reload_cancel_token->load(std::memory_order_acquire))
return;
json m_Res = json::object();
m_Res["command"] = "clear_3mf_info";
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id++);
m_Res["sequence_id"] = std::to_string(ProjectPanel::m_sequence_id.fetch_add(1, std::memory_order_relaxed));
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', false, json::error_handler_t::ignore));
wxGetApp().CallAfter([this, strJS] {
const auto cancel_token = m_reload_cancel_token;
wxGetApp().CallAfter([this, cancel_token, strJS] {
if (cancel_token->load(std::memory_order_acquire) || wxTheApp == nullptr || wxGetApp().is_closing())
return;
RunScript(strJS.ToStdString());
});
}
+8 -2
View File
@@ -26,9 +26,11 @@
#include "nlohmann/json.hpp"
#include "slic3r/Utils/json_diff.hpp"
#include <atomic>
#include <map>
#include <vector>
#include <memory>
#include <boost/thread.hpp>
#include "Event.hpp"
#include "libslic3r/ProjectTask.hpp"
#include "wxExtensions.hpp"
@@ -60,14 +62,17 @@ struct project_file{
class ProjectPanel : public wxPanel
{
private:
bool m_web_init_completed = {false};
std::atomic<bool> m_web_init_completed{false};
bool m_reload_already = {false};
std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_reload_task;
wxWebView* m_browser = {nullptr};
AuxiliaryPanel* m_auxiliary{nullptr};
wxString m_project_home_url;
wxString m_root_dir;
static inline int m_sequence_id = 8000;
static inline std::atomic<int> m_sequence_id{8000};
void show_info_editor(bool show);
@@ -75,6 +80,7 @@ private:
public:
ProjectPanel(wxWindow *parent, wxWindowID id = wxID_ANY, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize, long style = wxTAB_TRAVERSAL);
~ProjectPanel();
void shutdown();
void onWebNavigating(wxWebViewEvent& evt);
+7 -2
View File
@@ -1991,7 +1991,7 @@ void InputIpAddressDialog::workerThreadFunc(std::string str_ip, std::string str_
if (w.expired()) return;
if (m_obj) {
m_obj->set_user_access_code(str_access_code);
m_obj->set_access_code(str_access_code);
wxGetApp().getDeviceManager()->set_selected_machine(m_obj->get_dev_id());
}
@@ -2055,6 +2055,11 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
{
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
if (str_access_code.empty()) {
str_access_code = "88888888";
}
auto str_name = m_input_printer_name->GetTextCtrl()->GetValue().Strip(wxString::both);
auto str_sn = m_input_sn->GetTextCtrl()->GetValue().Strip(wxString::both);
bool invalid_access_code = true;
@@ -2062,7 +2067,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
for (char c : str_access_code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
invalid_access_code = false;
return;
break;
}
}
+29 -18
View File
@@ -111,18 +111,20 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox
sizer->Add(m_radio_group, 0, wxEXPAND | wxTOP | wxLEFT, BORDER_W);
std::string inherits_str = sel_preset.inherits();
if (parent->m_mode == comDevelop && !inherits_str.empty()) {
if (parent->m_mode == comDevelop) {
// A new user copy of a system preset inherits from the selected system preset.
const std::string parent_name = sel_preset.is_system ? sel_preset.name : sel_preset.inherits();
const bool can_detach = !parent_name.empty();
wxBoxSizer *detach_sizer = new wxBoxSizer(wxHORIZONTAL);
auto detach_tooltip = _L("Copies all inherited values from the parent preset into this preset and removes the connection with the parent preset.");
auto detach_tooltip = _L("Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported.");
auto detach_checkbox = new ::CheckBox(parent);
detach_checkbox->SetToolTip(detach_tooltip);
auto detach_label = new wxStaticText(parent, wxID_ANY, _L("Detach from parent"));
detach_label->SetFont(::Label::Body_14);
detach_label->SetForegroundColour(wxColour("#363636"));
detach_label->SetToolTip(detach_tooltip);
detach_sizer->Add(detach_checkbox, 0, wxALIGN_LEFT | wxLEFT, BORDER_W);
@@ -130,27 +132,36 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox
sizer->Add(detach_sizer, 0, wxEXPAND | wxTOP, BORDER_W);
sizer->AddSpacer(FromDIP(5));
auto parent_label = new wxStaticText(parent, wxID_ANY, inherits_str);
const wxString parent_text = can_detach ? from_u8(parent_name) : _L("Unique preset");
auto parent_label = new wxStaticText(parent, wxID_ANY, parent_text);
parent_label->SetFont(::Label::Body_12);
parent_label->SetForegroundColour(wxColour("#6B6B6B"));
parent_label->SetToolTip(_L("Parent preset"));
parent_label->SetToolTip(can_detach ? _L("Parent preset") : _L("This preset does not inherit from another preset."));
sizer->Add(parent_label, 0, wxEXPAND | wxLEFT, BORDER_W + FromDIP(24));
sizer->AddSpacer(FromDIP(5));
// Set initial state (unchecked by default)
detach_checkbox->SetValue(m_detach);
// Bind the checkbox event to update the detach state for this item
detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); });
if (!can_detach) {
detach_checkbox->Disable();
detach_label->SetForegroundColour(wxColour("#6B6B6B"));
}
else {
// Set initial state (unchecked by default)
detach_checkbox->SetValue(m_detach);
// Bind the checkbox event to update the detach state for this item
detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); });
auto on_toggle = [this, detach_checkbox]() {
detach_checkbox->SetValue(!detach_checkbox->GetValue());
wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId());
ev.SetEventObject(detach_checkbox);
detach_checkbox->GetEventHandler()->ProcessEvent(ev);
};
detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();});
detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();});
detach_label->SetForegroundColour(wxColour("#363636"));
auto on_toggle = [this, detach_checkbox]() {
detach_checkbox->SetValue(!detach_checkbox->GetValue());
wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId());
ev.SetEventObject(detach_checkbox);
detach_checkbox->GetEventHandler()->ProcessEvent(ev);
};
detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();});
detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();});
}
}
m_radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this](wxCommandEvent &e) {
+3 -3
View File
@@ -1088,8 +1088,8 @@ void SelectMachineDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &res
}
}
relayout_nozzle_cards();
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection();
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) {
wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
updata_thumbnail_data_after_connected_printer();
}
}
@@ -3912,7 +3912,7 @@ _collect_sorted_machines(Slic3r::DeviceManager* dev_manager,
};
// collect from user machine list
const auto& user_machine_list = dev_manager->get_my_machine_list();// user machine list
const auto& user_machine_list = dev_manager->get_my_machine_list(dev_manager->get_current_printer_agent_id());// user machine list
for (const auto& elem : user_machine_list)
{
MachineObject* mobj = elem.second;
+6 -2
View File
@@ -501,6 +501,7 @@ void SelectMachinePopup::update_other_devices()
DeviceManager* dev = wxGetApp().getDeviceManager();
if (!dev) return;
m_free_machine_list = dev->get_local_machinelist();
const std::string current_agent_id = dev->get_current_printer_agent_id();
BOOST_LOG_TRIVIAL(trace) << "SelectMachinePopup update_other_devices start";
this->Freeze();
@@ -512,6 +513,10 @@ void SelectMachinePopup::update_other_devices()
/* do not show printer bind state is empty */
if (!mobj->is_avaliable()) continue;
/* do not show devices discovered/bound by a different printer agent */
if (mobj->printer_agent_id != current_agent_id)
continue;
if (!wxGetApp().is_user_login(wxGetApp().get_printer_cloud_provider()) && !mobj->is_lan_mode_printer())
continue;
@@ -634,7 +639,7 @@ void SelectMachinePopup::update_user_devices()
}
m_bind_machine_list.clear();
m_bind_machine_list = dev->get_my_machine_list();
m_bind_machine_list = dev->get_my_machine_list(dev->get_current_printer_agent_id());
//sort list
std::vector<std::pair<std::string, MachineObject*>> user_machine_list;
@@ -704,7 +709,6 @@ void SelectMachinePopup::update_user_devices()
}
mobj->set_access_code("");
mobj->erase_user_access_code();
}
if (GUI::wxGetApp().plater())
+2 -2
View File
@@ -1218,8 +1218,8 @@ void SyncAmsInfoDialog::sync_ams_mapping_result(std::vector<FilamentInfo> &resul
iter++;
}
}
auto tab_index = (MainFrame::TabPosition) dynamic_cast<Notebook *>(wxGetApp().tab_panel())->GetSelection();
if (tab_index == MainFrame::TabPosition::tp3DEditor || tab_index == MainFrame::TabPosition::tpPreview) {
wxString tab_name = wxGetApp().tab_panel()->GetSelectedPageName();
if (tab_name == TAB_ID_PREPARE || tab_name == TAB_ID_PREVIEW) {
updata_thumbnail_data_after_connected_printer();
}
}
+8 -4
View File
@@ -2790,7 +2790,7 @@ void TabPrint::build()
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_patterns#sparse-infill-smooth-factor");
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
@@ -6458,7 +6458,7 @@ void Tab::load_current_preset()
std::string bmp_name = tab->type() == Slic3r::Preset::TYPE_FILAMENT ? "spool" :
tab->type() == Slic3r::Preset::TYPE_SLA_MATERIAL ? "" : "cog";
tab->Hide(); // #ys_WORKAROUND : Hide tab before inserting to avoid unwanted rendering of the tab
dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), tab, tab->title(), bmp_name);
dynamic_cast<Notebook*>(wxGetApp().tab_panel())->InsertPage(wxGetApp().tab_panel()->FindPage(this), wxString(), tab, tab->title(), bmp_name);
}
else
#endif
@@ -8537,8 +8537,12 @@ void Page::activate(ConfigOptionMode mode, std::function<void()> throw_if_cancel
#ifdef __WXMSW__
// BBS: fix field control position
wxTheApp->CallAfter([this]() {
for (auto group : m_optgroups) {
wxTheApp->CallAfter([wp = std::weak_ptr<Page>(shared_from_this())]() {
auto page = wp.lock();
if (!page)
return;
for (auto group : page->m_optgroups) {
if (group->custom_ctrl)
group->custom_ctrl->fixup_items_positions();
}
+4 -22
View File
@@ -108,7 +108,7 @@ public:
// by this control) and show it immediately.
bool ShowNewPage(wxWindow * page)
{
return AddPage(page, wxString(), ""/*true *//* select it */);
return AddPage(page, wxString());
}
// Set effect to use for showing/hiding pages.
@@ -139,14 +139,13 @@ public:
// Implement base class pure virtual methods.
// adds a new page to the control
bool AddPage(wxWindow* page,
const wxString& text,
const std::string& bmp_name,
bool bSelect = false)
bool bSelect = false,
int imageId = NO_IMAGE) override
{
DoInvalidateBestSize();
return InsertNewPage(GetPageCount(), page, text, bmp_name, bSelect);
return InsertPage(GetPageCount(), page, text, bSelect, imageId);
}
//// Page management
@@ -167,23 +166,6 @@ public:
return true;
}
bool InsertNewPage(size_t n,
wxWindow * page,
const wxString & text,
const std::string& bmp_name = "",
bool bSelect = false)
{
if (!wxBookCtrlBase::InsertPage(n, page, text, bSelect))
return false;
GetBtnsListCtrl()->InsertPage(n, text, bSelect, bmp_name);
if (bSelect)
SetSelection(n);
return true;
}
bool RemovePage(size_t n)
{
if (!wxBookCtrlBase::RemovePage(n))
+369 -91
View File
@@ -1,7 +1,9 @@
#include "WebGuideDialog.hpp"
#include "ConfigWizard.hpp"
#include <boost/algorithm/string/join.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/iostreams/detail/select.hpp>
#include <boost/log/trivial.hpp>
@@ -9,7 +11,9 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetCacheFormat.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "libslic3r_version.h"
@@ -41,8 +45,6 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI {
json m_ProfileJson;
static wxString update_custom_filaments()
{
json m_Res = json::object();
@@ -190,12 +192,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame()
{
m_destroy = true;
if (m_load_task && m_load_task->joinable()) {
*m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable())
m_load_task->join();
delete m_load_task;
m_load_task = nullptr;
}
m_load_task.reset();
if (m_browser) {
delete m_browser;
m_browser = nullptr;
@@ -301,15 +301,71 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/**
* Callback invoked when a navigation request was accepted
*/
// The empty shape every profile-loading path starts from or falls back to.
void GuideFrame::reset_profile_json()
{
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
}
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
reset_profile_json();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
orca_bundle_rsrc = true;
if (boost::filesystem::exists(vendor_dir)) {
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) &&
boost::iequals(entry.path().extension().string(), ".json") &&
!boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
}
}
}
auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json)
? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string()
: (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
}
void GuideFrame::on_profile_loaded()
{
// Must be called on the main thread.
SaveProfileData();
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
json res;
res["command"] = "userguide_profile_load_finish";
res["sequence_id"] = "10001";
RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true)));
}
void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
{
//wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
if (!bFirstComplete) {
m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this));
// boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this));
//LoadProfileThread.detach();
bFirstComplete = true;
try {
init_guide_paths();
if (BuildProfileDataFromPresetBundle()) {
if (!*m_cancel_token)
on_profile_loaded();
} else {
// Presets not yet in memory — delegate to background thread.
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what();
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
}
m_browser->Show();
@@ -762,11 +818,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
bool check_unsaved_preset_changes = false;
std::vector<std::string> install_bundles;
std::vector<std::string> remove_bundles;
const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
for (const auto &it : enabled_vendors) {
if (it.second.size() > 0) {
auto vendor_file = vendor_dir/(it.first + ".json");
if (!fs::exists(vendor_file)) {
if (!is_vendor_installed(it.first)) {
install_bundles.emplace_back(it.first);
}
}
@@ -777,8 +831,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
if (it.second.size() > 0) {
if (enabled_vendors.find(it.first) != enabled_vendors.end())
continue;
auto vendor_file = vendor_dir/(it.first + ".json");
if (fs::exists(vendor_file)) {
if (is_vendor_installed(it.first)) {
remove_bundles.emplace_back(it.first);
}
}
@@ -1127,99 +1180,324 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return status;
}
int GuideFrame::LoadProfileData()
bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors)
{
try {
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
// Models from vendor profiles
for (const auto& [vendor_id, vp] : bundle.vendors) {
for (const auto& model : vp.models) {
std::string nozzle_str;
for (const auto& v : model.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
const std::string materials_str = boost::algorithm::join(model.default_materials, ";");
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png"))
.make_preferred();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
// Orca: add custom as default
// Orca: add json logic for vendor bundle
orca_bundle_rsrc = true;
// search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
json entry;
entry["model"] = model.id;
entry["name"] = model.name;
entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
// load the default filament library first
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name)) {
m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
} else {
m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
}
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
// Machine map: preset name -> {model, nozzle variant}
for (const Preset& p : bundle.printers()) {
if (!p.is_system || !p.vendor) continue;
const auto* printer_model = p.config.option<ConfigOptionString>("printer_model");
const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant");
if (!printer_model || printer_model->value.empty() || !printer_variant) continue;
//load custom bundle from user data path
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy)
return 0;
json mach;
mach["model"] = printer_model->value;
mach["nozzle"] = printer_variant->value;
m_ProfileJson["machine"][p.name] = mach;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
// Filament map from system filament presets (vendor/type already resolved in config)
const json& machines = m_ProfileJson["machine"];
for (const Preset& p : bundle.filaments()) {
if (!p.is_system || !p.vendor) continue;
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type");
const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers");
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : "";
std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : "";
std::string model_list;
if (compat_printers) {
for (const std::string& pname : compat_printers->values) {
auto it = machines.find(pname);
if (it != machines.end()) {
const std::string m = (*it)["model"];
const std::string n = (*it)["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
if (m_destroy)
return 0;
json ff;
ff["name"] = p.name;
ff["sub_path"] = p.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][p.name] = ff;
}
wxGetApp().CallAfter([this] {
if (!m_destroy) {
//sync to appconfig first to populate current selections
SaveProfileData();
// Process list from visible system print presets
for (const Preset& p : bundle.prints()) {
if (!p.is_system || !p.vendor || !p.is_visible) continue;
json entry;
entry["name"] = p.name;
entry["sub_path"] = p.file;
m_ProfileJson["process"].push_back(entry);
}
//sync to web after selections are populated
std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
if (require_all_resource_vendors) {
// If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a
// packaged build ships instead) not covered by the current bundle, the
// bundle is incomplete (e.g. dev env where data_dir/system only has
// OrcaFilamentLibrary+Custom). Fall back so the slow path reads both dirs.
try {
for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) {
if (bundle.vendors.find(name) == bundle.vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name
<< "' in resources but not in preset_bundle — falling back to JSON loading";
reset_profile_json();
return false;
}
}
} catch (const std::exception&) {}
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll;
json m_Res = json::object();
m_Res["command"] = "userguide_profile_load_finish";
m_Res["sequence_id"] = "10001";
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true));
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson failed: " << e.what()
<< " — falling back to JSON loading";
reset_profile_json();
return false;
}
}
RunScript(strJS);
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true);
}
bool GuideFrame::BuildProfileDataFromVendors()
{
try {
// Same vendor set and precedence as the JSON scan in LoadProfileData: a
// vendor in the user's system dir shadows the bundled one of that name.
// vendor_names_in names a vendor by its profile or, where a build ships
// preset caches instead, by its cache alone.
std::map<std::string, boost::filesystem::path> vendor_sources;
for (const boost::filesystem::path& dir : { vendor_dir, rsrc_vendor_dir }) {
boost::system::error_code ec;
if (boost::filesystem::exists(dir, ec))
for (const std::string& name : vendor_names_in(dir))
vendor_sources.emplace(name, dir); // first dir wins
}
// The load order: the filament library first, because the others'
// filaments inherit from it, then every versioned vendor — each loaded
// from the directory it was found in, so a vendor that is not installed
// is served from the shipped profiles. Each is stamped by name and
// version alone: a profile change requires a version bump, so those two
// determine content wherever the vendor's copy sits.
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
std::vector<VendorSource> ordered;
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
// The version a load from `dir` would serve: the profile's where one
// exists (a cache is only served while it covers the profile beside
// it), the cache's own stamp where the cache is the whole vendor.
// A profile without a version (blacklist.json) carries no presets
// and is passed over.
const boost::filesystem::path profile = dir / (name + ".json");
if (boost::filesystem::exists(profile)) {
const Semver v = get_version_from_json(profile.string());
if (v.valid())
ordered.push_back({name, dir, v.to_string()});
} else {
ordered.push_back({name, dir,
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
}
};
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
add_vendor(filament_library, it->second);
for (const auto& [name, dir] : vendor_sources)
if (name != filament_library)
add_vendor(name, dir);
if (ordered.empty())
return false;
json stamps = json::array();
for (const VendorSource& v : ordered)
stamps.push_back({v.name, v.version});
// What this function derives is a pure function of that stamped set, so
// the derived JSON is cached whole: a fresh cache makes an open one
// file read, with no bundle built and no preset installed. Stale or
// absent, the bundle is rebuilt below and the result written back.
const boost::filesystem::path cache_file =
boost::filesystem::path(Slic3r::data_dir()) / "cache" / "wizard_profile_data.json";
try {
// Slurped whole and parsed from the buffer — nlohmann's fastest
// input path; a stream adapter costs real time on a multi-MB file.
boost::nowide::ifstream ifs(cache_file.string(), std::ios::binary);
if (ifs.is_open()) {
const std::string text{std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()};
json cached = json::parse(text);
if (cached.value("format", 0) == 1 && cached["vendors"] == stamps &&
! cached["profile"]["machine"].empty()) {
for (const char* key : { "model", "machine", "filament", "process" })
m_ProfileJson[key] = std::move(cached["profile"][key]);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: profile data served from " << cache_file;
return true;
}
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: rejecting cached profile data: " << e.what();
}
// Each vendor comes from its preset cache where one covers it, which is
// what makes this worth doing instead of the scan below; loading into a
// bundle per vendor keeps the install order the startup path has.
PresetBundle bundle;
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
const boost::filesystem::path& dir, const PresetBundle* base) {
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
};
for (const VendorSource& v : ordered) {
if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
if (v.name == filament_library) {
load_vendor(bundle, v.name, v.dir, nullptr);
} else {
PresetBundle tmp;
load_vendor(tmp, v.name, v.dir, &bundle);
bundle.merge_presets(std::move(tmp));
}
}
if (bundle.vendors.empty())
return false;
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
return false;
// Written through a temp file and moved into place, as the preset caches
// are: half a cache must never be readable, and the PID suffix keeps two
// instances from interleaving on one temp file.
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
try {
json out;
out["format"] = 1;
out["vendors"] = std::move(stamps);
json& profile = out["profile"];
for (const char* key : { "model", "machine", "filament", "process" })
profile[key] = m_ProfileJson[key];
boost::filesystem::create_directories(cache_file.parent_path());
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
ofs.close();
if (! ofs.good())
throw std::runtime_error("write failed");
}
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
throw std::runtime_error(ec.message());
} catch (const std::exception& e) {
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what();
reset_profile_json();
return false;
}
}
int GuideFrame::LoadProfileData()
{
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Loading order (fastest to slowest):
// 1. Load every vendor, from its preset cache wherever one covers it
// 2. Read all vendor JSONs by hand
try {
if (!BuildProfileDataFromVendors()) {
// Last resort — read all vendor JSONs
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name))
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
else
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (*m_cancel_token) return 0;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (*m_cancel_token) return 0;
}
}
// Capture the cancel token by value (shared_ptr) so the lambda doesn't
// touch `this` if GuideFrame is destroyed before the event fires.
auto tok = m_cancel_token;
wxGetApp().CallAfter([this, tok] {
if (!*tok)
on_profile_loaded();
});
} catch (std::exception& e) {
// wxLogMessage("GUIDE: load_profile_error %s ", e.what());
// wxMessageBox(e.what(), "", MB_OK);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what();
}
filament_info_cache.clear();
+16 -2
View File
@@ -30,10 +30,14 @@
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include <atomic>
#include <memory>
#include <unordered_map>
#include <nlohmann/json.hpp>
#include <boost/thread.hpp>
namespace Slic3r { namespace GUI {
class GuideFrame : public DPIDialog
@@ -78,6 +82,12 @@ public:
int LoadProfileData();
int SaveProfileData();
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths();
void on_profile_loaded();
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromVendors();
void reset_profile_json();
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -112,8 +122,11 @@ private:
//First Load
bool bFirstComplete{false};
bool m_destroy{false};
boost::thread* m_load_task{ nullptr };
// Set once in the destructor. Read through `this` by the loading thread
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
// lambdas so they don't touch `this` after the object is freed.
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_load_task;
// User Config
bool PrivacyUse;
@@ -123,6 +136,7 @@ private:
bool InstallNetplugin;
bool network_plugin_ready {false};
json m_ProfileJson;
json m_OrcaFilaList;
std::string m_OrcaFilaLibPath;
+7 -16
View File
@@ -95,14 +95,11 @@ void Button::SetIcon(const wxString& icon)
}
}
void Button::SetInactiveIcon(const wxString &icon)
void Button::SetIcon(const wxBitmap& icon)
{
if (!icon.IsEmpty()) {
// BBS set button icon default size to 20
this->inactive_icon = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt());
} else {
this->inactive_icon = ScalableBitmap();
}
this->active_icon = ScalableBitmap();
this->active_icon.bmp() = icon;
messureSize();
Refresh();
}
@@ -257,12 +254,10 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
void Button::Rescale()
{
if (this->active_icon.bmp().IsOk())
// Only a named icon can be re-rasterized; one set from a wxBitmap has no source file,
if (!this->active_icon.name().empty())
this->active_icon.msw_rescale();
if (this->inactive_icon.bmp().IsOk())
this->inactive_icon.msw_rescale();
messureSize();
if(m_has_style)
@@ -293,11 +288,7 @@ void Button::render(wxDC& dc)
wxSize szIcon;
wxSize textSize = this->textSize.GetSize();
ScalableBitmap icon;
if (m_selected || ((states & (int)StateColor::State::Hovered) != 0))
icon = active_icon;
else
icon = inactive_icon;
const ScalableBitmap& icon = active_icon;
wxSize padding = this->paddingSize;
int spacing = 5;
// Wrap text
+1 -3
View File
@@ -34,7 +34,6 @@ class Button : public StaticBox
wxSize minSize; // set by outer
wxSize paddingSize;
ScalableBitmap active_icon;
ScalableBitmap inactive_icon;
StateColor text_color;
@@ -61,8 +60,7 @@ public:
bool SetFont(const wxFont& font) override;
void SetIcon(const wxString& icon);
void SetInactiveIcon(const wxString& icon);
void SetIcon(const wxBitmap& icon);
void SetMinSize(const wxSize& size) override;
void SetMaxSize(const wxSize& size) override;
+41 -13
View File
@@ -57,18 +57,6 @@ std::string host_theme_vars_css()
return s;
}
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string host_theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return WebViewHostDialog::document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
// JS to re-theme an already-loaded document live (no reload): replace the injected
// style's contents and update data-orca-theme. Everything downstream (theme.css
// tokens, plugin element defaults, page layout) re-cascades from these values.
@@ -87,6 +75,46 @@ if(document.documentElement)
} // namespace
// Document-start user script: injects the contract <style>, stamps data-orca-theme before
// first paint, and raises a JS flag so the legacy globalapi.js dark.css poll stands down for
// host-themed pages. The WebView2 timing guard lives in document_start_injector().
std::string WebViewHostDialog::theme_user_script()
{
const std::string style = "<style id=\"orca-host-theme-vars\">" + host_theme_vars_css() + "</style>";
return document_start_injector(
style, "orca-host-theme-vars", "afterbegin",
"window.__orcaHostThemed=true;var theme=\"" + host_theme_name() + "\";",
"if(document.documentElement)document.documentElement.setAttribute('data-orca-theme',theme);");
}
std::string WebViewHostDialog::plugin_defaults_user_script()
{
std::string css;
css += "<style id=\"orca-plugin-defaults\">";
css += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
css += "body{margin:0;}";
css += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
css += "a{color:var(--orca-accent);}";
css += "hr{border:0;border-top:1px solid var(--orca-border);}";
css += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
css += "button:hover{filter:brightness(1.1);}";
css += "button:disabled{opacity:.5;cursor:default;}";
css += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
css += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
css += "table{border-collapse:collapse;}";
css += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
css += "th{color:var(--orca-muted);font-weight:600;}";
css += "::-webkit-scrollbar{width:12px;height:12px;}";
css += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
css += "::-webkit-scrollbar-track{background:transparent;}";
css += "</style>";
return document_start_injector(css, "orca-plugin-defaults", "beforeend");
}
std::string WebViewHostDialog::document_start_injector(const std::string& markup,
const char* dom_id,
const char* position,
@@ -244,7 +272,7 @@ void WebViewHostDialog::register_theme_user_scripts()
// script message handler is registered separately (AddScriptMessageHandler), but on
// some backends RemoveAllUserScripts() drops it too, which would break
// window.wx.postMessage / HandleStudio. Live re-theme goes through apply_theme_live().
m_browser->AddUserScript(wxString::FromUTF8(host_theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(theme_user_script()));
add_user_scripts();
}
@@ -49,6 +49,10 @@ public:
const std::string& prelude = {},
const std::string& on_inject = {});
// Shared by modeless Pages tabs and PluginWebDialog.
static std::string theme_user_script();
static std::string plugin_defaults_user_script();
protected:
wxWebView* browser() const { return m_browser; }
@@ -1379,7 +1379,6 @@ void MoonrakerPrinterAgent::announce_printhost_device()
if (auto* app_config = GUI::wxGetApp().app_config) {
const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key;
app_config->set_str("access_code", device_info.dev_id, access_code);
app_config->set_str("user_access_code", device_info.dev_id, access_code);
}
nlohmann::json payload;
+29 -32
View File
@@ -1044,46 +1044,42 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
std::set<std::string> bundles;
// Orca: always install filament library
bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
if (is_json_file(file_path)) {
const auto path_in_vendor = vendor_path / path.filename();
std::string vendor_name = path.filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
if (bundles.find(vendor_name) != bundles.end())continue;
// A vendor is named by its profile or, where the build ships preset caches
// instead of the raw profile JSONs, by its cache alone.
for (const std::string &vendor_name : vendor_names_in(rsrc_path)) {
if (bundles.find(vendor_name) != bundles.end())continue;
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
if (enabled_config_update) {
if ( fs::exists(path_in_vendor)) {
if (is_vendor_enabled) {
Semver resource_ver = get_version_from_json(file_path);
Semver vendor_ver = get_version_from_json(path_in_vendor.string());
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
if (enabled_config_update) {
if (is_vendor_installed(vendor_name)) {
if (is_vendor_enabled) {
// Orca: whichever form of the vendor resources ships at the newer
// version is the one installing lays down, and the one to judge
// what is installed against.
Semver resource_ver = resource_vendor_version(vendor_name);
// Orca: a vendor installed as a preset cache has no profile
// beside it; the version it was installed at is in the cache.
Semver vendor_ver = installed_vendor_version(vendor_name);
if (vendor_ver < resource_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
bundles.insert(vendor_name);
}
}
else {
//need to be removed because not installed
fs::remove(path_in_vendor);
const auto path_of_vendor = vendor_path / vendor_name;
if (fs::exists(path_of_vendor))
fs::remove_all(path_of_vendor);
if (vendor_ver < resource_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
bundles.insert(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
else {
//need to be removed because not installed
remove_installed_vendor(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
}
}
if (bundles.size() > 0) {
@@ -1163,11 +1159,12 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
if (( fs::exists(path_in_vendor))
if (is_vendor_installed(vendor_name)
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
Semver vendor_ver = get_version_from_json(path_in_vendor.string());
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);
+1 -1
View File
@@ -368,7 +368,7 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
emit_progress(100);
if (the_job.switch_to_device_tab) {
const auto mainframe = GUI::wxGetApp().mainframe;
mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor);
mainframe->request_select_tab(TAB_ID_MONITOR);
}
}
}
+1 -1
View File
@@ -325,7 +325,7 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path,
wxLaunchDefaultBrowser(url);
} else {
const auto mainframe = GUI::wxGetApp().mainframe;
mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor);
mainframe->request_select_tab(TAB_ID_MONITOR);
mainframe->load_printer_url(url);
}
+16 -14
View File
@@ -19,6 +19,7 @@
#include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/pages/PagesPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
@@ -319,17 +320,17 @@ void bind_python_api(pybind11::module_& m)
{
m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation)
.value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Pages", PluginCapabilityType::Pages)
.value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
.value("Success", PluginResult::Success)
@@ -419,9 +420,10 @@ void bind_python_api(pybind11::module_& m)
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
PrinterAgentPluginCapability::RegisterBindings(m);
PagesPluginCapability::RegisterBindings(m);
ScriptPluginCapability::RegisterBindings(m);
SlicingPipelinePluginCapability::RegisterBindings(m);
PluginHost::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
+5 -5
View File
@@ -12,7 +12,7 @@
namespace Slic3r {
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
struct PluginCapabilityId
{
@@ -39,7 +39,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{
switch (type) {
case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation";
case PluginCapabilityType::Pages: return "pages";
case PluginCapabilityType::Analysis: return "analysis";
case PluginCapabilityType::Importer: return "importer";
case PluginCapabilityType::Exporter: return "exporter";
@@ -54,7 +54,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
{
switch (type) {
case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation";
case PluginCapabilityType::Pages: return "Pages";
case PluginCapabilityType::Analysis: return "Analysis";
case PluginCapabilityType::Importer: return "Importer";
case PluginCapabilityType::Exporter: return "Exporter";
@@ -76,8 +76,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection;
if (lowered == "automation")
return PluginCapabilityType::Automation;
if (lowered == "pages")
return PluginCapabilityType::Pages;
if (lowered == "analysis")
return PluginCapabilityType::Analysis;
if (lowered == "importer")
+469
View File
@@ -0,0 +1,469 @@
#include "PluginPages.hpp"
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Notebook.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/WebView.hpp"
#include "slic3r/GUI/Widgets/WebViewHostDialog.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/Utils.hpp>
#include <algorithm>
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#include <nlohmann/json.hpp>
#include <stdexcept>
#include <wx/bookctrl.h>
#include <wx/menu.h>
#include <wx/sizer.h>
#include <utility>
namespace Slic3r {
namespace {
constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function deliver(payload, attempts) {
try {
if (window.wx && typeof window.wx.postMessage === 'function') {
window.wx.postMessage(payload);
return;
}
} catch (e) { /* retry while the native handler is being registered */ }
if (attempts < 100)
window.setTimeout(function () { deliver(payload, attempts + 1); }, 25);
}
function send(data) {
deliver(JSON.stringify({
channel: 'orca', kind: 'message', data: (data === undefined ? null : data)
}), 0);
}
window.orca = {
postMessage: function (data) { send(data); },
onMessage: function (callback) {
if (typeof callback === 'function') handlers.push(callback);
}
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
} // namespace
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
, m_cap(std::move(capability))
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
{
auto* topsizer = new wxBoxSizer(wxVERTICAL);
SetSizer(topsizer);
m_browser = WebView::CreateWebView(this, bootstrap_url());
if (m_browser == nullptr) {
wxLogError("Could not initialize plugin page web view");
return;
}
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginPage::on_bootstrap_event, this);
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginPage::on_bootstrap_event, this);
m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, &PluginPage::on_new_window, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginPage::on_script_message, this);
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(GUI::WebViewHostDialog::plugin_defaults_user_script()));
m_browser->AddUserScript(PLUGIN_PAGE_BRIDGE_JS);
const std::shared_ptr<std::atomic<PluginPage*>> lifetime = m_lifetime;
m_cap->set_message_sender([lifetime](const std::string& message) {
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([lifetime, message] {
if (PluginPage* page = lifetime->load(std::memory_order_acquire))
page->push_message(message);
});
});
}
PluginPage::~PluginPage()
{
detach_capability();
if (m_lifetime)
m_lifetime->store(nullptr, std::memory_order_release);
}
void PluginPage::detach_capability()
{
if (m_lifetime)
m_lifetime->store(nullptr, std::memory_order_release);
if (m_cap)
m_cap->clear_message_sender();
m_cap.reset();
}
wxString PluginPage::web_base_url() const
{
const auto path = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + GUI::from_u8(path) + "/";
}
wxString PluginPage::bootstrap_url() const
{
const auto path = (boost::filesystem::path(resources_dir()) / "web/dialog/PluginWebDialog/blank.html").make_preferred().string();
return wxString("file://") + GUI::from_u8(path);
}
void PluginPage::on_bootstrap_event(wxWebViewEvent& event)
{
load_plugin_content();
event.Skip();
}
void PluginPage::load_plugin_content()
{
if (m_content_loaded || m_browser == nullptr || m_cap == nullptr)
return;
m_content_loaded = true;
try {
m_browser->SetPage(wxString::FromUTF8(m_cap->get_ui()), web_base_url());
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "': " << error.what();
detach_capability();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Failed to load plugin page '" << m_cap->name() << "'";
detach_capability();
}
}
void PluginPage::on_new_window(wxWebViewEvent& event)
{
const wxString url = event.GetURL();
if (!url.empty() && m_browser != nullptr)
m_browser->LoadURL(url);
event.Veto();
}
void PluginPage::on_script_message(wxWebViewEvent& event)
{
if (!m_cap)
return;
const wxString payload = event.GetString();
nlohmann::json root = nlohmann::json::parse(payload.utf8_string(), nullptr, false);
if (root.is_discarded() || root.value("channel", std::string()) != "orca" ||
root.value("kind", std::string()) != "message")
return;
const auto data = root.find("data");
try {
m_cap->on_message(data == root.end()
? "null"
: data->dump(-1, ' ', false, nlohmann::json::error_handler_t::replace));
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "': " << error.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin page message handler failed for '" << m_cap->name() << "'";
}
}
void PluginPage::push_message(const std::string& message)
{
if (m_browser == nullptr)
return;
// PagesPluginCapability::post_message() already dumps JSON, so accept it as-is; only a
// non-JSON payload needs wrapping as a string literal.
const std::string payload = nlohmann::json::accept(message)
? message
: nlohmann::json(message).dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
WebView::RunScript(m_browser, wxString::Format(
"(function dispatch(payload, attempts) {\n"
" if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n"
" if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n"
"})({data: %s}, 0);",
wxString::FromUTF8(payload)));
}
PluginPages::~PluginPages()
{
shutdown();
}
void PluginPages::initialize(Notebook* parent)
{
shutdown();
m_parent = parent;
if (m_parent == nullptr)
return;
m_visible_page_count = GUI::wxGetApp().app_config->get_plugin_pages_visible_count();
for (const auto& capability : PluginManager::instance().get_plugin_capabilities("", PluginCapabilityType::Pages)) {
if (capability)
create_page(capability->identity());
}
relayout();
}
void PluginPages::shutdown()
{
while (!m_pages.empty())
remove_page(m_pages.begin()->first);
m_parent = nullptr;
}
void PluginPages::set_visible_page_count(int count)
{
const int clamped = std::clamp(count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
if (clamped == m_visible_page_count)
return;
m_visible_page_count = clamped;
relayout();
}
std::shared_ptr<PagesPluginCapability> PluginPages::get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const
{
auto capability = PluginManager::instance().get_plugin_capability(id, /*only_enabled=*/false);
if (!capability || capability->is_enabled() != is_enabled || capability->type() != PluginCapabilityType::Pages)
return nullptr;
return std::dynamic_pointer_cast<PagesPluginCapability>(capability);
}
bool PluginPages::create_page(const PluginCapabilityId& id)
{
if (m_pages.find(id) != m_pages.end())
return false;
auto capability = get_pages_cap(id, true);
if (!capability)
return false;
std::string icon;
try {
icon = capability->get_icon();
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key << ": " << error.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to get icon for plugin " << id.plugin_key;
}
auto* page = new PluginPage(m_parent, std::move(capability));
if (!page->is_valid()) {
page->Destroy();
return false;
}
if (!icon.empty()) {
try {
boost::filesystem::path icon_path(icon);
const std::string extension = icon_path.extension().string();
if (extension == ".svg" || extension == ".png")
icon_path.replace_extension();
page->set_icon(create_scaled_bitmap(icon_path.string(), m_parent, 20));
} catch (const std::exception& error) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key << ": " << error.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to load icon for plugin " << id.plugin_key;
}
}
m_pages.emplace(id, page);
m_order.push_back(id);
return true;
}
void PluginPages::on_cap_register(const PluginCapabilityId& id)
{
if (m_parent == nullptr)
return;
if (create_page(id))
relayout();
}
void PluginPages::on_cap_deregister(const PluginCapabilityId& id)
{
remove_page(id);
}
void PluginPages::on_plugin_register(const std::string& plugin_key)
{
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) {
if (capability)
on_cap_register(capability->identity());
}
}
void PluginPages::on_plugin_deregister(const std::string& plugin_key)
{
for (auto it = m_pages.begin(); it != m_pages.end();) {
if (it->first.plugin_key != plugin_key) {
++it;
continue;
}
const PluginCapabilityId id = it->first;
++it;
remove_page(id);
}
}
void PluginPages::remove_page(const PluginCapabilityId& id)
{
auto it = m_pages.find(id);
if (it == m_pages.end())
return;
PluginPage* page = it->second;
page->detach_capability();
m_pages.erase(it);
m_order.erase(std::remove(m_order.begin(), m_order.end(), id), m_order.end());
const int idx = m_parent != nullptr ? m_parent->FindPage(page) : wxNOT_FOUND;
if (idx != wxNOT_FOUND)
m_parent->RemovePage(idx);
relayout();
page->Destroy();
}
wxString PluginPages::page_tab_id(const PluginCapabilityId& id)
{
return wxString::FromUTF8("plugin." + id.plugin_key + "." + id.name);
}
void PluginPages::relayout()
{
if (m_parent == nullptr)
return;
m_order.erase(std::remove_if(m_order.begin(), m_order.end(),
[this](const PluginCapabilityId& id) {
const bool orphaned = m_pages.find(id) == m_pages.end();
if (orphaned)
BOOST_LOG_TRIVIAL(error) << "PluginPages::relayout: '" << id.name << "' was in m_order but not m_pages, dropping";
return orphaned;
}),
m_order.end());
const int visible_slots = std::max(1, m_visible_page_count);
const bool need_overflow = static_cast<int>(m_order.size()) > visible_slots;
// Every visible slot is a normal, individual tab hosting its own page. When there's
// overflow, the last slot's page is swappable via m_overflow_button/show_overflow_menu()
// rather than being a fixed page — m_swapped_in_id tracks which one currently sits there.
std::vector<PluginCapabilityId> tab_ids;
if (!need_overflow) {
tab_ids = m_order;
m_swapped_in_id.reset();
} else {
const auto overflow_begin = m_order.begin() + (visible_slots - 1);
tab_ids.assign(m_order.begin(), overflow_begin);
if (!m_swapped_in_id || std::find(overflow_begin, m_order.end(), *m_swapped_in_id) == m_order.end())
m_swapped_in_id = *overflow_begin;
tab_ids.push_back(*m_swapped_in_id);
}
// MainFrame::show_device() relayouts on every printer change and most of those change
// nothing, so only touch the notebook when the trailing slots don't already spell out
// tab_ids — a rebuild destroys and recreates every tab button and rasterizes every icon.
const size_t page_count = m_parent->GetPageCount();
bool up_to_date = page_count >= tab_ids.size();
for (size_t i = 0; up_to_date && i < tab_ids.size(); ++i)
up_to_date = m_parent->GetPageName(page_count - tab_ids.size() + i) == page_tab_id(tab_ids[i]);
for (const auto& [id, page] : m_pages) {
if (!up_to_date)
break;
const bool wanted = std::find(tab_ids.begin(), tab_ids.end(), id) != tab_ids.end();
up_to_date = (m_parent->FindPage(page) != wxNOT_FOUND) == wanted;
}
if (!up_to_date) {
const wxString id_to_reselect = m_parent->GetSelectedPageName();
for (const auto& [id, page] : m_pages) {
const int idx = m_parent->FindPage(page);
if (idx != wxNOT_FOUND)
m_parent->RemovePage(idx);
}
for (const auto& id : tab_ids) {
PluginPage* page = m_pages.at(id);
m_parent->InsertPage(m_parent->GetPageCount(), page_tab_id(id), page, wxString::FromUTF8(id.name), "",
false, page->icon());
}
if (!id_to_reselect.empty())
m_parent->SelectPageByName(id_to_reselect);
}
if (need_overflow) {
if (m_overflow_button == nullptr) {
auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER);
btn->SetCornerRadius(0);
const int em = em_unit(m_parent);
btn->SetMinSize({40 * em / 10, 36 * em / 10});
btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_overflow_menu(); });
GUI::wxGetApp().UpdateDarkUI(btn);
m_overflow_button = btn;
}
m_parent->SetOverflowButton(m_overflow_button);
} else if (m_overflow_button != nullptr) {
m_parent->SetOverflowButton(nullptr);
m_overflow_button->Destroy();
m_overflow_button = nullptr;
}
}
void PluginPages::show_overflow_menu()
{
const int visible_slots = std::max(1, m_visible_page_count);
if (m_overflow_button == nullptr || static_cast<int>(m_order.size()) <= visible_slots)
return;
const std::vector<PluginCapabilityId> overflow_ids(m_order.begin() + (visible_slots - 1), m_order.end());
wxMenu menu;
for (size_t i = 0; i < overflow_ids.size(); ++i)
menu.AppendRadioItem(static_cast<int>(wxID_HIGHEST + 1 + i), wxString::FromUTF8(overflow_ids[i].name));
if (m_swapped_in_id) {
const auto it = std::find(overflow_ids.begin(), overflow_ids.end(), *m_swapped_in_id);
if (it != overflow_ids.end())
menu.Check(static_cast<int>(wxID_HIGHEST + 1 + (it - overflow_ids.begin())), true);
}
menu.Bind(wxEVT_MENU, [this, overflow_ids](wxCommandEvent& evt) {
const size_t index = static_cast<size_t>(evt.GetId() - (wxID_HIGHEST + 1));
if (index >= overflow_ids.size())
return;
m_swapped_in_id = overflow_ids[index];
relayout();
m_parent->SelectPageByName(page_tab_id(*m_swapped_in_id));
});
m_overflow_button->PopupMenu(&menu);
}
} // namespace Slic3r
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/pages/PagesPluginCapability.hpp>
#include <atomic>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <wx/bitmap.h>
#include <wx/panel.h>
#include <wx/webview.h>
class Notebook;
namespace Slic3r {
class PluginPage : public wxPanel
{
public:
PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability);
~PluginPage() override;
PluginPage() = delete;
bool is_valid() const { return m_browser != nullptr && m_cap != nullptr; }
void detach_capability();
void on_bootstrap_event(wxWebViewEvent& event);
void on_new_window(wxWebViewEvent& event);
void on_script_message(wxWebViewEvent& event);
void push_message(const std::string& message);
void set_icon(const wxBitmap& icon) { m_icon = icon; }
const wxBitmap& icon() const { return m_icon; }
private:
void load_plugin_content();
wxString bootstrap_url() const;
wxString web_base_url() const;
wxWebView* m_browser{nullptr};
std::shared_ptr<PagesPluginCapability> m_cap;
std::shared_ptr<std::atomic<PluginPage*>> m_lifetime;
bool m_content_loaded{false};
wxBitmap m_icon;
};
class PluginPages
{
public:
PluginPages() = default;
~PluginPages();
PluginPages(const PluginPages&) = delete;
PluginPages& operator=(const PluginPages&) = delete;
void initialize(Notebook* parent);
void shutdown();
void on_cap_register(const PluginCapabilityId& id);
void on_cap_deregister(const PluginCapabilityId& id);
void on_plugin_register(const std::string& plugin_key);
void on_plugin_deregister(const std::string& plugin_key);
void set_visible_page_count(int count);
void relayout();
private:
std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
bool create_page(const PluginCapabilityId& id);
void remove_page(const PluginCapabilityId& id);
void show_overflow_menu();
static wxString page_tab_id(const PluginCapabilityId& id);
std::map<PluginCapabilityId, PluginPage*> m_pages;
std::vector<PluginCapabilityId> m_order;
Notebook* m_parent{nullptr};
int m_visible_page_count{0};
std::optional<PluginCapabilityId> m_swapped_in_id;
wxWindow* m_overflow_button{nullptr};
};
} // namespace Slic3r
@@ -0,0 +1,60 @@
#include "PagesPluginCapability.hpp"
#include "PagesPluginCapabilityTrampoline.hpp"
#include "../../PluginFsUtils.hpp"
#include <boost/log/trivial.hpp>
#include <pybind11/pybind11.h>
#include <utility>
namespace py = pybind11;
namespace Slic3r {
void PagesPluginCapability::RegisterBindings(pybind11::module_& module)
{
BOOST_LOG_TRIVIAL(debug) << "Registering orca.pages bindings";
auto pages = module.def_submodule("pages", "Plugin page API");
py::class_<PagesPluginCapability, PluginCapabilityInterface, PyPagesPluginCapabilityTrampoline,
std::shared_ptr<PagesPluginCapability>>(pages, "PagesPluginCapabilityBase")
.def(py::init<>())
.def("get_type", &PagesPluginCapability::get_type)
.def("get_ui", &PagesPluginCapability::get_ui)
.def("get_icon", &PagesPluginCapability::get_icon)
.def("on_message", &PagesPluginCapability::on_message)
.def(
"post_message",
[](PagesPluginCapability& capability, py::object data) {
capability.post_message(py_to_json(data).dump());
},
py::arg("data"), "Send a JSON-compatible value to the page's window.orca.onMessage handlers.");
}
void PagesPluginCapability::post_message(std::string message)
{
std::function<void(const std::string&)> sender;
{
std::lock_guard<std::mutex> lock(m_message_mutex);
sender = m_message_sender;
}
if (sender)
sender(message);
}
void PagesPluginCapability::set_message_sender(std::function<void(const std::string&)> sender)
{
std::lock_guard<std::mutex> lock(m_message_mutex);
m_message_sender = std::move(sender);
}
void PagesPluginCapability::clear_message_sender()
{
std::lock_guard<std::mutex> lock(m_message_mutex);
m_message_sender = nullptr;
}
} // namespace Slic3r
@@ -0,0 +1,33 @@
#ifndef slic3r_PagesPluginCapability_hpp_
#define slic3r_PagesPluginCapability_hpp_
#include "../../PythonPluginInterface.hpp"
#include "pybind11/pybind11.h"
#include <functional>
#include <mutex>
#include <string>
namespace Slic3r {
class PagesPluginCapability : public PluginCapabilityInterface
{
public:
static void RegisterBindings(pybind11::module_& module);
PluginCapabilityType get_type() const override { return PluginCapabilityType::Pages; }
virtual std::string get_ui() = 0;
virtual void on_message(std::string message) { (void) message; }
virtual std::string get_icon() { return {}; }
void post_message(std::string message);
void set_message_sender(std::function<void(const std::string&)> sender);
void clear_message_sender();
private:
mutable std::mutex m_message_mutex;
std::function<void(const std::string&)> m_message_sender;
};
} // namespace Slic3r
#endif
@@ -0,0 +1,59 @@
#pragma once
#include "PagesPluginCapability.hpp"
#include "../../PluginFsUtils.hpp"
#include "../../PyPluginTrampoline.hpp"
#include <nlohmann/json.hpp>
namespace Slic3r {
class PyPagesPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PagesPluginCapability>
{
public:
using PyPluginCommonTrampoline<PagesPluginCapability>::PyPluginCommonTrampoline;
std::string get_icon() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
std::string,
PagesPluginCapability,
get_icon);
}
std::string get_ui() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
PagesPluginCapability,
get_ui);
}
void on_message(std::string message) override
{
PluginCapabilityInterface::RefCounter ref_counter(*this);
PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
if (!override)
return;
nlohmann::json data = nlohmann::json::parse(message, nullptr, false);
if (data.is_discarded())
data = message;
ORCA_PY_LOGGED_OVERRIDE_BODY(override(::Slic3r::json_to_py(data)));
}
};
} // namespace Slic3r
@@ -13,10 +13,8 @@ namespace py = pybind11;
namespace Slic3r {
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module)
{
(void) pluginTypes;
auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")

Some files were not shown because too many files have changed in this diff Show More