mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-21 06:21:16 +00:00
Merge branch 'main' into fix/arrange-scale-fit-lay-on-face
This commit is contained in:
commit
017521ba78
27 files changed
+394
-100
No files matched your search
+25
-3
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
u )
|
||||
export BUILD_TARGET="universal"
|
||||
;;
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d]"
|
||||
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
|
||||
echo " -d: Build deps only"
|
||||
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
|
||||
echo " -s: Build slicer only"
|
||||
@@ -60,13 +60,33 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
|
||||
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
|
||||
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
|
||||
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
|
||||
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
|
||||
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
|
||||
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
|
||||
exit 0
|
||||
;;
|
||||
: )
|
||||
echo "Option -${OPTARG} requires an argument, aborting."
|
||||
exit 1
|
||||
;;
|
||||
* )
|
||||
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
|
||||
if [[ ${OPTIND} -le $# ]] ; then
|
||||
separator_index=$((OPTIND-1))
|
||||
if [[ "${!separator_index}" != "--" ]] ; then
|
||||
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
shift $((OPTIND-1))
|
||||
EXTRA_CMAKE_ARGS=("$@")
|
||||
|
||||
# Set defaults
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
@@ -211,7 +231,8 @@ function build_deps() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target deps
|
||||
)
|
||||
@@ -314,7 +335,8 @@ function build_slicer() {
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
|
||||
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
|
||||
"${CMAKE_CCACHE_ARGS[@]}" \
|
||||
${CMAKE_POLICY_COMPAT}
|
||||
${CMAKE_POLICY_COMPAT} \
|
||||
"${EXTRA_CMAKE_ARGS[@]}"
|
||||
fi
|
||||
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
|
||||
)
|
||||
|
||||
Vendored
+6
-2
@@ -59,9 +59,13 @@ else ()
|
||||
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
||||
endif ()
|
||||
|
||||
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
|
||||
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
|
||||
# silicon whenever CGAL falls back to exact arithmetic.
|
||||
ExternalProject_Add(dep_GMP
|
||||
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
|
||||
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
||||
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
||||
BUILD_IN_SOURCE ON
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Flashforge",
|
||||
"url": "",
|
||||
"version": "02.04.00.11",
|
||||
"version": "02.04.00.12",
|
||||
"force_update": "0",
|
||||
"description": "Flashforge configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M 0.4 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M Pro 0.4 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[0]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M 0.6 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M Pro 0.6 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
@@ -226,10 +226,10 @@ modules:
|
||||
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
||||
dest: external-packages/OpenVDB
|
||||
|
||||
# GMP 6.2.1
|
||||
# GMP 6.3.0
|
||||
- type: file
|
||||
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
dest: external-packages/GMP
|
||||
|
||||
# MPFR 4.2.2
|
||||
|
||||
@@ -2914,7 +2914,7 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
|
||||
const float delta = float(scale_(t));
|
||||
// Arc tolerance of 1/500 of the offset, Clipper2's default.
|
||||
for (const Polygon &ring : offset(polylines, delta, jtRound, 0.002 * delta, etOpenRound))
|
||||
for (const Polygon &ring : offset(polylines, delta, jtMiter, 0.002 * delta, etOpenRound))
|
||||
if (ring.size() >= 3)
|
||||
all_polylines.emplace_back(ring.split_at_first_point());
|
||||
}
|
||||
|
||||
+14
-14
@@ -3979,22 +3979,22 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->placeholder_parser().set("hold_chamber_temp_for_flat_print", new ConfigOptionBool(hold_chamber_temp_for_flat_print));
|
||||
}
|
||||
|
||||
this->placeholder_parser().set("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
this->placeholder_parser().set_deferred("print_time_total_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_day", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_hour", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_minute", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder));
|
||||
this->placeholder_parser().set_deferred("used_filament_length", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
|
||||
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
|
||||
// The running extruded_* totals are left alone; profiles use them as numbers.
|
||||
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
|
||||
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
|
||||
this->placeholder_parser().set_deferred("used_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("normal_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("silent_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_weight", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_wipe_tower_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_wipe_tower_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder));
|
||||
|
||||
// Sync variant-mapped params into placeholder_parser before processing start gcode
|
||||
update_placeholder_parser_with_variant_params();
|
||||
|
||||
@@ -243,7 +243,7 @@ namespace client
|
||||
struct expr
|
||||
{
|
||||
expr() {}
|
||||
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range)
|
||||
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range), m_deferred(rhs.m_deferred)
|
||||
{ if (rhs.type() == TYPE_STRING) m_data.s = new std::string(*rhs.m_data.s); else m_data.set(rhs.m_data); }
|
||||
expr(expr &&rhs) : expr(std::move(rhs), rhs.it_range.begin(), rhs.it_range.end()) {}
|
||||
|
||||
@@ -258,13 +258,16 @@ namespace client
|
||||
explicit expr(std::string &&s) : m_type(TYPE_STRING) { m_data.s = new std::string(std::move(s)); }
|
||||
explicit expr(const std::string &s, const Iterator &it_begin, const Iterator &it_end) :
|
||||
m_type(TYPE_STRING), it_range(it_begin, it_end) { m_data.s = new std::string(s); }
|
||||
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }
|
||||
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }, m_deferred(rhs.m_deferred)
|
||||
{
|
||||
m_data.set(rhs.m_data);
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
rhs.m_deferred = Deferred::None;
|
||||
}
|
||||
expr &operator=(const expr &rhs)
|
||||
{
|
||||
// set_s() clears m_deferred, which is also rhs.m_deferred on a self-assignment.
|
||||
const Deferred deferred = rhs.m_deferred;
|
||||
if (rhs.type() == TYPE_STRING) {
|
||||
this->set_s(rhs.s());
|
||||
} else {
|
||||
@@ -272,6 +275,7 @@ namespace client
|
||||
m_data.set(rhs.m_data);
|
||||
}
|
||||
this->it_range = rhs.it_range;
|
||||
m_deferred = deferred;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -281,8 +285,10 @@ namespace client
|
||||
this->reset();
|
||||
m_type = rhs.type();
|
||||
this->it_range = rhs.it_range;
|
||||
m_deferred = rhs.m_deferred;
|
||||
m_data.set(rhs.m_data);
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
rhs.m_deferred = Deferred::None;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -291,7 +297,8 @@ namespace client
|
||||
{
|
||||
if (this->type() == TYPE_STRING)
|
||||
delete m_data.s;
|
||||
m_type = TYPE_EMPTY;
|
||||
m_type = TYPE_EMPTY;
|
||||
m_deferred = Deferred::None;
|
||||
}
|
||||
~expr() { reset(); }
|
||||
|
||||
@@ -304,6 +311,10 @@ namespace client
|
||||
};
|
||||
Type type() const { return m_type; }
|
||||
bool numeric_type() const { return m_type == TYPE_INT || m_type == TYPE_DOUBLE; }
|
||||
// A deferred string is either one deferred value or text that contains at least one.
|
||||
enum class Deferred : unsigned char { None, Value, Text };
|
||||
bool deferred() const { return m_deferred != Deferred::None; }
|
||||
void set_deferred(Deferred deferred) { m_deferred = deferred; }
|
||||
|
||||
bool& b() { return m_data.b; }
|
||||
bool b() const { return m_data.b; }
|
||||
@@ -323,18 +334,21 @@ namespace client
|
||||
std::string& s() { return *m_data.s; }
|
||||
const std::string& s() const { return *m_data.s; }
|
||||
void set_s(const std::string &s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = s;
|
||||
else
|
||||
this->set_s_take_ownership(new std::string(s));
|
||||
}
|
||||
void set_s(std::string &&s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = std::move(s);
|
||||
else
|
||||
this->set_s_take_ownership(new std::string(std::move(s)));
|
||||
}
|
||||
void set_s(const char *s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = s;
|
||||
else
|
||||
@@ -478,12 +492,21 @@ namespace client
|
||||
{
|
||||
if (this->type() == TYPE_EMPTY) {
|
||||
// Inside an if / else block to be skipped.
|
||||
} else if (m_deferred == Deferred::Value && (rhs.type() != TYPE_STRING || rhs.m_deferred == Deferred::Value)) {
|
||||
// A deferred value added to a number or to another deferred value is meant as a sum.
|
||||
this->throw_if_deferred();
|
||||
} else if (rhs.m_deferred == Deferred::Value && this->type() != TYPE_STRING) {
|
||||
rhs.throw_if_deferred();
|
||||
} else if (this->type() == TYPE_STRING) {
|
||||
// Convert the right hand side to string and append.
|
||||
*m_data.s += rhs.to_string();
|
||||
if (this->deferred() || rhs.deferred())
|
||||
m_deferred = Deferred::Text;
|
||||
} else if (rhs.type() == TYPE_STRING) {
|
||||
// Conver the left hand side to string, append rhs.
|
||||
this->set_s(this->to_string() + rhs.s());
|
||||
if (rhs.deferred())
|
||||
m_deferred = Deferred::Text;
|
||||
} else {
|
||||
const char *err_msg = L("Cannot add non-numeric types.");
|
||||
this->throw_if_not_numeric(err_msg);
|
||||
@@ -602,6 +625,8 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
rhs.throw_if_deferred();
|
||||
bool value = false;
|
||||
if (lhs.numeric_type() && rhs.numeric_type()) {
|
||||
// Both types are numeric.
|
||||
@@ -731,6 +756,7 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
const std::string *subject = nullptr;
|
||||
if (lhs.type() == TYPE_STRING) {
|
||||
// One type is string, the other could be converted to string.
|
||||
@@ -761,6 +787,7 @@ namespace client
|
||||
if (subject.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
subject.throw_if_deferred();
|
||||
if (subject.type() != TYPE_STRING)
|
||||
subject.throw_exception(L("regex_replace() first parameter must be a string."));
|
||||
try {
|
||||
@@ -785,6 +812,8 @@ namespace client
|
||||
return;
|
||||
}
|
||||
if (! out.b()) {
|
||||
match.throw_if_deferred();
|
||||
pattern.throw_if_deferred();
|
||||
if (match.type() != TYPE_STRING)
|
||||
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
||||
if (pattern.type() != TYPE_STRING)
|
||||
@@ -818,6 +847,8 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
rhs.throw_if_deferred();
|
||||
bool value = false;
|
||||
if (lhs.type() == TYPE_BOOL && rhs.type() == TYPE_BOOL) {
|
||||
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
|
||||
@@ -830,8 +861,16 @@ namespace client
|
||||
static void logical_or (expr &lhs, expr &rhs) { logical_op(lhs, rhs, '|'); }
|
||||
static void logical_and(expr &lhs, expr &rhs) { logical_op(lhs, rhs, '&'); }
|
||||
|
||||
static const char* deferred_message()
|
||||
{
|
||||
return L("This value is known only after the G-code is generated. It can be printed, but not used in a calculation or a comparison.");
|
||||
}
|
||||
|
||||
// Reports the deferred message for a type error on a deferred value, since it holds a tag in place of the value.
|
||||
void throw_exception(const char *message) const
|
||||
{
|
||||
if (this->deferred())
|
||||
message = deferred_message();
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
this->it_range.begin(), this->it_range.end(), spirit::info(std::string("*") + message)));
|
||||
}
|
||||
@@ -842,11 +881,18 @@ namespace client
|
||||
this->throw_exception(message);
|
||||
}
|
||||
|
||||
void throw_if_deferred() const
|
||||
{
|
||||
if (this->deferred())
|
||||
this->throw_exception(deferred_message());
|
||||
}
|
||||
|
||||
private:
|
||||
// This object will take ownership of the parameter string object "s".
|
||||
void set_s_take_ownership(std::string* s) { assert(this->type() != TYPE_STRING); Data tmp; tmp.s = s; m_data.set(tmp); m_type = TYPE_STRING; }
|
||||
|
||||
Type m_type = TYPE_EMPTY;
|
||||
Deferred m_deferred = Deferred::None;
|
||||
|
||||
union Data {
|
||||
bool b;
|
||||
@@ -895,6 +941,8 @@ namespace client
|
||||
std::string error_message;
|
||||
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
||||
mutable std::set<std::string> inactive_local_variables;
|
||||
// Tags of PlaceholderParser::set_deferred(), matched by value so that a copy in a variable is deferred too.
|
||||
const std::set<std::string> *deferred_tags = nullptr;
|
||||
|
||||
// Table to translate symbol tag to a human readable error message.
|
||||
static std::map<std::string, std::string> tag_to_error_message;
|
||||
@@ -970,6 +1018,18 @@ namespace client
|
||||
} else
|
||||
this->config_local.set_key_value(opt_key, opt.release());
|
||||
}
|
||||
void mark_deferred(expr &output) const {
|
||||
if (output.type() != expr::TYPE_STRING)
|
||||
return;
|
||||
expr::Deferred deferred = expr::Deferred::None;
|
||||
if (deferred_tags != nullptr && output.s().find('@') != std::string::npos)
|
||||
for (const std::string &tag : *deferred_tags)
|
||||
if (output.s().find(tag) != std::string::npos) {
|
||||
deferred = output.s() == tag ? expr::Deferred::Value : expr::Deferred::Text;
|
||||
break;
|
||||
}
|
||||
output.set_deferred(deferred);
|
||||
}
|
||||
|
||||
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
|
||||
{
|
||||
@@ -1430,6 +1490,7 @@ namespace client
|
||||
vector_element_to_expr(ctx, opt, output);
|
||||
else
|
||||
scalar_variable_to_expr(ctx, opt, output);
|
||||
ctx->mark_deferred(output);
|
||||
}
|
||||
output.it_range = opt.it_range;
|
||||
}
|
||||
@@ -1516,7 +1577,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_vector())
|
||||
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
|
||||
ctx->throw_exception(L("Cannot assign a scalar value to a vector variable."), rhs.it_range);
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
|
||||
} else {
|
||||
@@ -1542,7 +1603,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_scalar())
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
|
||||
} else {
|
||||
@@ -1568,7 +1629,7 @@ namespace client
|
||||
if (! ctx->skipping()) {
|
||||
check_writable(ctx, lhs);
|
||||
if (lhs.opt->is_scalar())
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
|
||||
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
|
||||
}
|
||||
}
|
||||
@@ -1803,10 +1864,21 @@ namespace client
|
||||
|
||||
static void filament_change(const MyContext* ctx, expr& param)
|
||||
{
|
||||
if (param.type() == expr::TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
MyContext *context = const_cast<MyContext *>(ctx);
|
||||
param.throw_if_deferred();
|
||||
context->current_extruder_id = param.as_i();
|
||||
}
|
||||
|
||||
// The replacement may put a deferred tag into the result.
|
||||
static void regex_replace(const MyContext *ctx, expr &subject, IteratorRange &pattern, expr &replacement)
|
||||
{
|
||||
expr::regex_replace(subject, pattern, replacement);
|
||||
ctx->mark_deferred(subject);
|
||||
}
|
||||
|
||||
static void throw_exception(const std::string &msg, const IteratorRange &it_range)
|
||||
{
|
||||
// An asterix is added to the start of the string to differentiate the boost::spirit::info::tag content
|
||||
@@ -2409,7 +2481,7 @@ namespace client
|
||||
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
||||
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
|
||||
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
|
||||
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
|
||||
[ px::bind(&MyContext::regex_replace, _r1, _val, _2, _3) ]
|
||||
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
|
||||
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
|
||||
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
|
||||
@@ -2614,6 +2686,7 @@ std::string PlaceholderParser::process(const std::string &templ, unsigned int cu
|
||||
context.config = &this->config();
|
||||
context.config_override = config_override;
|
||||
context.config_outputs = config_outputs;
|
||||
context.deferred_tags = &m_deferred_tags;
|
||||
context.current_extruder_id = current_extruder_id;
|
||||
context.context_data = context_data;
|
||||
return process_macro(templ, context);
|
||||
|
||||
@@ -60,6 +60,9 @@ public:
|
||||
void set(const std::string &key, double value) { this->set(key, new ConfigOptionFloat(value)); }
|
||||
void set(const std::string &key, const std::vector<std::string> &values) { this->set(key, new ConfigOptionStrings(values)); }
|
||||
void set(const std::string &key, ConfigOption *opt) { m_config.set_key_value(key, opt); }
|
||||
// Orca: sets a value known only after the G-code is generated, as a tag that post-processing replaces.
|
||||
// A template may print it or join it to text, and a calculation or a comparison on it throws.
|
||||
void set_deferred(const std::string &key, const std::string &tag) { this->set(key, tag); m_deferred_tags.insert(tag); }
|
||||
DynamicConfig& config_writable() { return m_config; }
|
||||
const DynamicConfig& config() const { return m_config; }
|
||||
const ConfigOption* option(const std::string &key) const { return m_config.option(key); }
|
||||
@@ -88,6 +91,7 @@ private:
|
||||
// config has a higher priority than external_config when looking up a symbol.
|
||||
DynamicConfig m_config;
|
||||
const DynamicConfig *m_external_config;
|
||||
std::set<std::string> m_deferred_tags;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -6459,7 +6459,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Seam gap");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("In order to reduce the visibility of the seam in a closed loop extrusion, the loop is interrupted and shortened by a specified amount.\n"
|
||||
"This amount can be specified in millimeters or as a percentage of the current extruder diameter. The default value for this parameter is 10%.");
|
||||
"This amount can be specified in millimeters or as a percentage of the nozzle diameter. The default value for this parameter is 10%.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
|
||||
@@ -5632,14 +5632,10 @@ void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_mov
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double shift_z = mo->get_instance_min_z(i.second);
|
||||
//BBS: don't call translate if the z is zero
|
||||
if ((current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if (mo->auto_drop && (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5763,14 +5759,10 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int> &i : done) {
|
||||
ModelObject *mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BBS: don't call translate if the z is zero
|
||||
const double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if ((min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if (mo->auto_drop && (min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5865,14 +5857,10 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//BBS: don't call translate if the z is zero
|
||||
double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5985,14 +5973,10 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//BBS: don't call translate if the z is zero
|
||||
double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD)&&(shift_z != 0.0f)) {
|
||||
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
|
||||
@@ -3002,7 +3002,8 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
};
|
||||
|
||||
// why: a newly loaded plugin only adds a selectable agent
|
||||
// refresh the dropdown and leave the live agent alone
|
||||
// refresh the dropdown and leave the live agent alone, unless the selected preset was
|
||||
// waiting for this agent: plugins load after the startup preset is applied
|
||||
auto refresh_printer_agent_dropdown_after_load = [](const std::string&)
|
||||
{
|
||||
if (!wxTheApp)
|
||||
@@ -3014,8 +3015,11 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
|
||||
app->CallAfter([app]
|
||||
{
|
||||
if (!app->is_closing())
|
||||
app->refresh_printer_agent_dropdown();
|
||||
if (app->is_closing())
|
||||
return;
|
||||
|
||||
app->switch_printer_agent_if_pending();
|
||||
app->refresh_printer_agent_dropdown();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4303,6 +4307,21 @@ void GUI_App::switch_printer_agent()
|
||||
select_machine(effective_agent_id);
|
||||
}
|
||||
|
||||
void GUI_App::switch_printer_agent_if_pending()
|
||||
{
|
||||
if (!m_agent || !preset_bundle)
|
||||
return;
|
||||
|
||||
const std::string wanted_agent_id = resolve_printer_agent_id(preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent"));
|
||||
const auto live_agent = m_agent->get_printer_agent();
|
||||
if (live_agent && live_agent->get_agent_info().id == wanted_agent_id)
|
||||
return;
|
||||
if (!NetworkAgentFactory::get_printer_agent_info(wanted_agent_id))
|
||||
return;
|
||||
|
||||
switch_printer_agent();
|
||||
}
|
||||
|
||||
void GUI_App::select_machine(const std::string& agent_id)
|
||||
{
|
||||
// Skip for BBL agent for now - uses its own device discovery/selection
|
||||
|
||||
@@ -412,6 +412,9 @@ public:
|
||||
|
||||
// Reconcile the live printer agent with the stored preset selection.
|
||||
void switch_printer_agent();
|
||||
// Install the selected preset's agent if it is registered but not live, e.g. when its plugin
|
||||
// finishes loading after the startup preset was applied. Otherwise leave the live agent alone.
|
||||
void switch_printer_agent_if_pending();
|
||||
|
||||
std::string resolve_printer_agent_id(const std::string& stored_id) const;
|
||||
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
|
||||
|
||||
@@ -1618,7 +1618,9 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
if (obj_idx != -1) {
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||
ModelObject* mo = (*m_objects)[obj_idx];
|
||||
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
notify_instance_updated(obj_idx);
|
||||
@@ -6734,7 +6736,9 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||
ModelObject* mo = (*m_objects)[obj_idx];
|
||||
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
notify_instance_updated(obj_idx);
|
||||
|
||||
@@ -2865,7 +2865,8 @@ Sidebar::Sidebar(Plater *parent)
|
||||
|
||||
// add printer title
|
||||
scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0);
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) {
|
||||
// ORCA Bind with LEFT_DOWN, since closing a dialog over the title bar can send it a stray LEFT_UP
|
||||
auto toggle_printer_section = ([this] (wxMouseEvent & e) {
|
||||
if (!p || !p->combo_printer || !p->m_text_printer_settings || !p->m_panel_printer_content || !m_scrolled_sizer)
|
||||
return;
|
||||
// ORCA Show printer name on title when its folded to inform user without expanding it again
|
||||
@@ -2876,6 +2877,8 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->m_panel_printer_separator->Show(isShown);
|
||||
m_scrolled_sizer->Layout();
|
||||
});
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_DOWN , toggle_printer_section);
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_DCLICK, toggle_printer_section);
|
||||
// ORCA add bottom border for seperation wile sections folded
|
||||
p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string
|
||||
p->m_panel_printer_separator->SetBackgroundColour("#FFFFFF");
|
||||
@@ -3248,7 +3251,8 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE);
|
||||
p->m_panel_filament_title->SetBackgroundColor(title_bg);
|
||||
p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1);
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) {
|
||||
// ORCA Bind with LEFT_DOWN, since closing a dialog over the title bar can send it a stray LEFT_UP
|
||||
auto toggle_filament_section = ([this](wxMouseEvent &e) {
|
||||
if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn)
|
||||
return;
|
||||
// ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button
|
||||
@@ -3267,6 +3271,8 @@ Sidebar::Sidebar(Plater *parent)
|
||||
|
||||
CallAfter([this]{update_filaments_counter(true);}); // call after all UI processing done
|
||||
});
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_DOWN , toggle_filament_section);
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_DCLICK, toggle_filament_section);
|
||||
|
||||
wxBoxSizer* bSizer39;
|
||||
bSizer39 = new wxBoxSizer( wxHORIZONTAL );
|
||||
|
||||
@@ -580,29 +580,12 @@ void Selection::drop()
|
||||
|
||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||
|
||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& volume = *(*m_volumes)[i];
|
||||
// Skip the wipe tower: its synthetic id (>= 1000) is not an index into m_model->objects.
|
||||
if (volume.object_idx() >= 1000)
|
||||
continue;
|
||||
ModelObject* model_object = m_model->objects[volume.object_idx()];
|
||||
|
||||
if (model_object != nullptr) {
|
||||
if (m_mode == Volume) {
|
||||
ModelVolume* cur_mv = model_object->volumes[volume.volume_idx()];
|
||||
cur_mv->set_transformation(volume.get_volume_transformation());
|
||||
} else if (m_mode == Instance) {
|
||||
model_object->instances[volume.instance_idx()]->set_transformation(volume.get_instance_transformation());
|
||||
}
|
||||
|
||||
model_object->invalidate_bounding_box();
|
||||
this->notify_instance_update(volume.object_idx(), volume.instance_idx());
|
||||
}
|
||||
}
|
||||
|
||||
wxGetApp().plater()->get_view3D_canvas3D()->post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
|
||||
// do_move() writes every volume back to the model and refreshes the view.
|
||||
setup_cache();
|
||||
TransformationType transformation_type;
|
||||
transformation_type.set_relative();
|
||||
translate(Vec3d(0, 0, -min_z), transformation_type);
|
||||
wxGetApp().plater()->get_view3D_canvas3D()->do_move("");
|
||||
}
|
||||
|
||||
void Selection::center_plate(const int plate_idx) {
|
||||
|
||||
@@ -310,7 +310,7 @@ Label::Label(wxWindow *parent, wxFont const &font, wxString const &text, long st
|
||||
SetBackgroundColour(StaticBox::GetParentBackgroundColor(parent));
|
||||
SetForegroundColour("#262E30");
|
||||
if (style & LB_PROPAGATE_MOUSE_EVENT) {
|
||||
for (auto evt : { wxEVT_LEFT_UP, wxEVT_LEFT_DOWN })
|
||||
for (auto evt : { wxEVT_LEFT_UP, wxEVT_LEFT_DOWN, wxEVT_LEFT_DCLICK}) // ORCA also propagete LEFT_DCLICK event that used on sidebar titlebars
|
||||
Bind(evt, [this] (auto & e) { GetParent()->GetEventHandler()->ProcessEventLocally(e); });
|
||||
};
|
||||
if (style & LB_AUTO_WRAP) {
|
||||
|
||||
@@ -73,7 +73,14 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
|
||||
// 3. Fetch slot info and build AmsTrayData directly
|
||||
std::vector<AmsTrayData> trays;
|
||||
int box_count = 0;
|
||||
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) {
|
||||
bool slots_ok = false;
|
||||
try {
|
||||
slots_ok = fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error);
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
// A slot value of an unexpected type fails the sync instead of escaping to the UI
|
||||
error = e.what();
|
||||
}
|
||||
if (!slots_ok) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
|
||||
return false;
|
||||
}
|
||||
@@ -91,7 +98,7 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
int& box_count,
|
||||
std::string& error)
|
||||
{
|
||||
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables&multi_color_controller=box");
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
|
||||
}
|
||||
@@ -164,9 +171,12 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = i;
|
||||
|
||||
// Read slot variables
|
||||
const int color_index = variables.value("color_slot" + std::to_string(i), 1);
|
||||
const int filament_type = variables.value("filament_slot" + std::to_string(i), 1);
|
||||
// Read slot variables. Newer firmware stores a hex color list and a material id instead of dictionary
|
||||
// indexes, and only in the slots it has rewritten, so each slot is checked for both forms.
|
||||
const auto color_value = variables.value("color_slot" + std::to_string(i), nlohmann::json(1));
|
||||
const auto filament_value = variables.value("filament_slot" + std::to_string(i), nlohmann::json(1));
|
||||
const int color_index = color_value.is_number() ? color_value.get<int>() : 0;
|
||||
const int filament_type = filament_value.is_number() ? filament_value.get<int>() : 0;
|
||||
const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0);
|
||||
|
||||
// Check filament presence via runout sensor
|
||||
@@ -187,6 +197,13 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
if (filament_it != dict.filaments.end()) {
|
||||
filament_name = filament_it->second;
|
||||
}
|
||||
if (filament_value.is_string()) {
|
||||
const nlohmann::json::json_pointer decoded_type("/multi_color_controller/box/box" + std::to_string(i / 4) +
|
||||
"_filament_info_" + std::to_string(i % 4) + "_filament_type");
|
||||
if (status.contains(decoded_type) && status[decoded_type].is_string()) {
|
||||
filament_name = status[decoded_type].get<std::string>();
|
||||
}
|
||||
}
|
||||
tray.tray_type = normalize_filament_type(filament_name);
|
||||
|
||||
// Try Qidi-specific setting ID first; fall back to visible preset by type
|
||||
@@ -202,7 +219,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
|
||||
// Look up color from dictionary
|
||||
auto color_it = dict.colors.find(color_index);
|
||||
if (color_it != dict.colors.end()) {
|
||||
if (color_value.is_array() && !color_value.empty() && color_value[0].is_string()) {
|
||||
tray.tray_color = color_value[0].get<std::string>();
|
||||
} else if (color_it != dict.colors.end()) {
|
||||
tray.tray_color = color_it->second;
|
||||
} else {
|
||||
tray.tray_color = "FFFFFFFF";
|
||||
|
||||
@@ -350,9 +350,13 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path,
|
||||
if (should_open_in_external_browser()) {
|
||||
wxLaunchDefaultBrowser(url);
|
||||
} else {
|
||||
const auto mainframe = GUI::wxGetApp().mainframe;
|
||||
mainframe->request_select_tab(TAB_ID_MONITOR);
|
||||
mainframe->load_printer_url(url);
|
||||
// The tab switch queues a reload of the printer's home page, so switch synchronously
|
||||
// first and queue the import page after it.
|
||||
GUI::wxGetApp().CallAfter([url = wxString::FromUTF8(url)] {
|
||||
const auto mainframe = GUI::wxGetApp().mainframe;
|
||||
mainframe->select_tab(TAB_ID_MONITOR);
|
||||
mainframe->load_printer_url(url);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -353,16 +353,18 @@ void PluginAuditManager::add_denied_filename(const std::string& filename)
|
||||
std::vector<std::string> PluginAuditManager::default_denied_filenames()
|
||||
{
|
||||
// AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
|
||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||
// on its mode, and is a non-static member we cannot call without an instance. Denying all
|
||||
// four names is cheaper and more robust than replicating that; the two unused names cost one
|
||||
// string comparison each. Single-sourced here so install_hook() and the tests seed from the
|
||||
// same list and cannot drift apart.
|
||||
// string comparison each. Keep the plugin install-state sidecar here too: it controls which
|
||||
// packages load on startup and must remain host-managed. Single-sourced so install_hook() and
|
||||
// the tests seed from the same list and cannot drift apart.
|
||||
return {
|
||||
SLIC3R_APP_KEY ".conf",
|
||||
GCODEVIEWER_APP_KEY ".conf",
|
||||
SLIC3R_APP_KEY ".ini",
|
||||
GCODEVIEWER_APP_KEY ".ini",
|
||||
secret_constants::USER_SECRET_FILENAME,
|
||||
".install_state.json",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -498,6 +500,12 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
candidate = absolute_candidate;
|
||||
}
|
||||
|
||||
// Importing Python modules reads their source and may inspect package directories. These
|
||||
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
|
||||
// and exclude third-party packages from the stdlib roots.
|
||||
if (!is_write && is_stdlib_path(candidate))
|
||||
return {true, ""};
|
||||
|
||||
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
|
||||
// delete-shaped one falls through to "outside allowed root" for that root even though the
|
||||
// path is physically inside it.
|
||||
@@ -1015,6 +1023,22 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, b
|
||||
|
||||
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||
|
||||
static const char* audit_category_name(AuditEventCategory category)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead: return "FsRead";
|
||||
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
|
||||
case AuditEventCategory::FsCreate: return "FsCreate";
|
||||
case AuditEventCategory::FsDelete: return "FsDelete";
|
||||
case AuditEventCategory::Http: return "Http";
|
||||
case AuditEventCategory::Socket: return "Socket";
|
||||
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
|
||||
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
|
||||
case AuditEventCategory::Threading: return "Threading";
|
||||
default: return "None";
|
||||
}
|
||||
}
|
||||
|
||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||
// no target (by_event), which the prompt does not show as a target.
|
||||
@@ -1032,6 +1056,16 @@ bool request_permission(AuditEventCategory category, const std::string& plugin_k
|
||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||
std::string targets_text = by_event ? "<none>" : "";
|
||||
for (const auto& target : shown) {
|
||||
if (!targets_text.empty())
|
||||
targets_text += ", ";
|
||||
targets_text += target;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
|
||||
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
|
||||
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
|
||||
|
||||
if (s_permission_prompt_for_testing)
|
||||
return s_permission_prompt_for_testing(event_name, shown);
|
||||
@@ -1198,6 +1232,18 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename)
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
|
||||
{
|
||||
if (path.empty())
|
||||
return false;
|
||||
|
||||
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
|
||||
return std::any_of(dirs.begin(), dirs.end(),
|
||||
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
|
||||
};
|
||||
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
|
||||
}
|
||||
|
||||
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||
{
|
||||
if (frames.empty())
|
||||
@@ -1266,7 +1312,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
||||
|
||||
void PluginAuditManager::install_hook()
|
||||
{
|
||||
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
|
||||
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
|
||||
PluginAuditDetail::collect_stdlib_dirs();
|
||||
|
||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||
@@ -1283,8 +1329,9 @@ void PluginAuditManager::install_hook()
|
||||
// "cert" denied-path keyword seeded below.
|
||||
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
|
||||
|
||||
// The user's app config and cloud credentials live directly inside data_dir(), so the
|
||||
// root just granted would otherwise expose them to any plugin. Deny them by name.
|
||||
// The user's app config and cloud credentials live directly inside data_dir(), and plugin
|
||||
// install state lives beneath it, so the root just granted would otherwise expose them to
|
||||
// plugins. Deny them by name.
|
||||
//
|
||||
// Seeded here rather than by each secret's owner because install_hook() runs during lazy
|
||||
// interpreter init and therefore provably precedes any plugin bytecode, whereas
|
||||
|
||||
@@ -173,6 +173,11 @@ private:
|
||||
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
|
||||
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
|
||||
// symlinks that can change.
|
||||
static bool is_stdlib_path(const boost::filesystem::path& path);
|
||||
|
||||
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
||||
void release_frames(std::vector<PyObject*>& frames);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
@@ -794,6 +795,20 @@ TEST_CASE("Print statistics in custom G-code match the final print statistics",
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Print statistics in custom G-code cannot be used in a calculation", "[Print]")
|
||||
{
|
||||
const std::string key = GENERATE(as<std::string>{}, "total_weight", "used_filament", "total_cost", "print_time", "normal_print_time",
|
||||
"silent_print_time", "total_wipe_tower_cost", "total_wipe_tower_filament", "print_time_total_sec",
|
||||
"print_time_day", "print_time_hour", "print_time_minute", "print_time_sec", "used_filament_length");
|
||||
CAPTURE(key);
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({ { "file_start_gcode", "; {" + key + " + 1}\n" } });
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
CHECK_THROWS_WITH(Slic3r::Test::gcode(print), Catch::Matchers::ContainsSubstring("known only after the G-code is generated"));
|
||||
}
|
||||
|
||||
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
|
||||
{
|
||||
Print print;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include "libslic3r/PlaceholderParser.hpp"
|
||||
@@ -398,3 +399,53 @@ SCENARIO("Placeholder parser names in branches that are not taken", "[Placeholde
|
||||
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Placeholder parser deferred values can be printed but not computed with", "[PlaceholderParser]") {
|
||||
PlaceholderParser parser;
|
||||
parser.set_deferred("total_weight", "@TOTAL_WEIGHT@");
|
||||
parser.set_deferred("used_filament", "@USED_FILAMENT_LENGTH@");
|
||||
parser.set("label", "W");
|
||||
PlaceholderParser::ContextData context;
|
||||
context.global_config = std::make_unique<DynamicConfig>();
|
||||
auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); };
|
||||
|
||||
SECTION("printed") {
|
||||
CHECK(process("{total_weight}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("[total_weight]") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{label + \": \" + total_weight + \"g\"}") == "W: @TOTAL_WEIGHT@g");
|
||||
CHECK(process("{total_weight + \"g, \" + used_filament + \"m\"}") == "@TOTAL_WEIGHT@g, @USED_FILAMENT_LENGTH@m");
|
||||
CHECK(process("{\"W \" + total_weight + \"g for \" + 3 + \" parts\"}") == "W @TOTAL_WEIGHT@g for 3 parts");
|
||||
CHECK(process("{true ? total_weight : 0}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{regex_replace(\"x\", /x/, total_weight)}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{local w = total_weight}{w}") == "@TOTAL_WEIGHT@");
|
||||
}
|
||||
SECTION("used in a calculation or a comparison") {
|
||||
const auto deferred_error = Catch::Matchers::ContainsSubstring("known only after the G-code is generated");
|
||||
for (const std::string templ : {
|
||||
"{total_weight + 1}", "{1 + total_weight}", "{total_weight * 2}", "{-total_weight}", "{int(total_weight)}",
|
||||
"{min(total_weight, 5)}", "{digits(total_weight, 5)}", "{total_weight + used_filament}",
|
||||
"{if total_weight > 100}heavy{endif}", "{if total_weight == \"1\"}one{endif}", "{total_weight and true}",
|
||||
"{total_weight ? 1 : 0}", "{if total_weight =~ /1.*/}one{endif}", "{one_of(total_weight, \"1\")}",
|
||||
"{regex_replace(total_weight, /1/, \"2\")}", "{(\"W\" + total_weight) == \"W1\"}", "{filament_change(total_weight)}",
|
||||
"{regex_replace(\"x\", /x/, total_weight) * 2}", "{if regex_replace(\"x\", /x/, \"W\" + total_weight) > \"W1\"}heavy{endif}",
|
||||
// A deferred value copied to a variable is still deferred.
|
||||
"{local w = total_weight}{w > 1}", "{global g = \"W\" + total_weight}{if g == \"W1\"}one{endif}" }) {
|
||||
CAPTURE(templ);
|
||||
CHECK_THROWS_WITH(process(templ), deferred_error);
|
||||
}
|
||||
// A mistake unrelated to the value being deferred keeps its own message.
|
||||
CHECK_THROWS_WITH(process("{global v = (1, 2)}{global v = \"W\" + total_weight}"),
|
||||
Catch::Matchers::ContainsSubstring("Cannot assign a scalar value to a vector variable"));
|
||||
}
|
||||
SECTION("ignored in a branch that is not taken") {
|
||||
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
|
||||
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
|
||||
PlaceholderParser::check_inactive_branches = true;
|
||||
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
|
||||
}
|
||||
SECTION("other strings keep their behavior") {
|
||||
CHECK(process("{label + 1}") == "W1");
|
||||
CHECK(process("{if label > \"A\"}yes{endif}") == "yes");
|
||||
CHECK(process("{if regex_replace(label, /X/, total_weight) == \"W\"}yes{endif}") == "yes");
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ void seed_denied_keywords()
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
|
||||
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
|
||||
{
|
||||
seed_denied_names();
|
||||
const PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
@@ -81,6 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
|
||||
CHECK(mgr.is_denied_filename(fs::path(SLIC3R_APP_KEY ".ini")));
|
||||
CHECK(mgr.is_denied_filename(fs::path(GCODEVIEWER_APP_KEY ".ini")));
|
||||
CHECK(mgr.is_denied_filename(fs::path(secret_constants::USER_SECRET_FILENAME)));
|
||||
CHECK(mgr.is_denied_filename(fs::path(".install_state.json")));
|
||||
}
|
||||
|
||||
SECTION("companions holding the same secrets are denied by the prefix rule")
|
||||
@@ -136,6 +137,7 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
||||
|
||||
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
|
||||
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
|
||||
const fs::path install_state = fs::path(data_dir()) / "plugins" / "example" / ".install_state.json";
|
||||
|
||||
SECTION("a non-denied file inside the allowed root is writable (root really grants writes)")
|
||||
{
|
||||
@@ -176,6 +178,17 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
|
||||
CHECK_FALSE(decision.allowed);
|
||||
}
|
||||
|
||||
SECTION("plugin install state is blocked for reads and writes despite the allowed root")
|
||||
{
|
||||
AuditDecision read_decision = mgr.check_open(install_state.string(), "r");
|
||||
CHECK_FALSE(read_decision.allowed);
|
||||
CHECK(read_decision.reason == "denied filename");
|
||||
|
||||
AuditDecision write_decision = mgr.check_open(install_state.string(), "w");
|
||||
CHECK_FALSE(write_decision.allowed);
|
||||
CHECK(write_decision.reason == "denied filename");
|
||||
}
|
||||
|
||||
SECTION("a traversal path resolving to the config is blocked")
|
||||
{
|
||||
const fs::path traversal = fs::path(data_dir()) / "plugins" / ".." / (SLIC3R_APP_KEY ".conf");
|
||||
@@ -1119,3 +1132,37 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
||||
}
|
||||
|
||||
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
|
||||
{
|
||||
AuditHarness harness;
|
||||
if (!harness.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
harness.add_plugin(PLUGIN);
|
||||
py::gil_scoped_acquire gil;
|
||||
PythonSide python(harness);
|
||||
|
||||
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
|
||||
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
|
||||
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
|
||||
// os.py ships with every CPython installation. A site-packages path is third-party code even
|
||||
// when the directory is empty: the prompt must come before the missing file is noticed.
|
||||
const fs::path stdlib_file = stdlib / "os.py";
|
||||
REQUIRE(fs::exists(stdlib_file));
|
||||
|
||||
python.globals["STDLIB_FILE"] = stdlib_file.string();
|
||||
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
|
||||
python.run_as(PLUGIN, R"PY(
|
||||
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
|
||||
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
|
||||
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
|
||||
)PY");
|
||||
|
||||
CHECK(python.str("read_outcome") == "ok");
|
||||
CHECK(python.str("write_outcome") == "denied");
|
||||
CHECK(python.str("site_packages_outcome") == "denied");
|
||||
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
|
||||
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
|
||||
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
|
||||
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
|
||||
}
|
||||
Reference in new issue
Block a user