Merge upstream main: rib wall prime tower, wipe tower sync, multi-extruder config fixes, plugin resolution fixes

This commit is contained in:
Clifford Garwood
2026-07-31 15:09:53 -04:00
27 changed files with 2377 additions and 1468 deletions
+18 -3
View File
@@ -2,6 +2,7 @@
#define slic3r_Config_hpp_
#include <assert.h>
#include <algorithm>
#include <map>
#include <climits>
#include <cfloat>
@@ -780,10 +781,14 @@ public:
this->values[i] = rhs_vec->values[i];
modified = true;
} else {
if ((i < default_index.size()) && (default_index[i] < default_value.size()))
// Orca: a negative slot (failed variant lookup) must not silently collapse the
// whole array to the first slot's value — the int-vs-size_t comparison used to
// promote -1 past the bounds check. Keep the slot's own value (get_at-style
// clamp) when no valid index is available.
if ((i < default_index.size()) && (default_index[i] >= 0) && (size_t(default_index[i]) < default_value.size()))
this->values[i] = default_value[default_index[i]];
else
this->values[i] = default_value[0];
this->values[i] = default_value[std::min(i, default_value.size() - 1)];
}
}
return modified;
@@ -2106,6 +2111,11 @@ public:
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
// rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum<T>
this->value = rhs->getInt();
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
// adopt the source's so a later serialize() can emit names.
if (this->keys_map == nullptr)
if (auto rhs_generic = dynamic_cast<const ConfigOptionEnumGeneric *>(rhs))
this->keys_map = rhs_generic->keys_map;
}
std::string serialize() const override
@@ -2162,7 +2172,12 @@ public:
if (rhs->type() != this->type())
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
// rhs could be of the following type: ConfigOptionEnumsGeneric
this->values = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs)->values;
auto rhs_enums = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs);
this->values = rhs_enums->values;
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
// adopt the source's so a later serialize() emits names instead of empty tokens.
if (this->keys_map == nullptr)
this->keys_map = rhs_enums->keys_map;
}
std::string serialize() const override
+132 -41
View File
@@ -14,6 +14,7 @@
#include "GCode/PrintExtents.hpp"
#include "GCode/Thumbnails.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "ShortestPath.hpp"
#include "GCode/OrderingStrategies.hpp"
#include "Print.hpp"
@@ -891,6 +892,65 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return res;
}
// Type2 tower-local point -> bed frame. The rib-wall offset is tower-local, so it
// rotates with the tower (unlike the BBL tower in append_tcr, which never rotates).
Vec2f WipeTowerIntegration::transform_wt2_pt(const Vec2f &pt) const
{
const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI);
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
}
// Printable-area bounds for tower-approach routing, in object coordinates (shared by
// the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router).
// Multi-nozzle: clamp the travel bounds to the region every extruder can reach
// (get_extruder_shared_printable_polygon) instead of the full bed. Gated on the
// multi-nozzle predicate so every existing single/dual printer keeps the historic
// full-printable_area routing byte-identical.
BoundingBox WipeTowerIntegration::printer_travel_bounds(GCode &gcodegen) const
{
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
BoundingBox printer_bbx;
if (is_multi_nozzle_printer(gcodegen.m_config)) {
printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon());
printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d);
printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d);
} else {
Points bed_points;
for (const auto& p : gcodegen.m_config.printable_area.values)
bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d));
printer_bbx = BoundingBox(bed_points);
}
return printer_bbx;
}
// With skip points enabled the Type2 tower wall has an opening at each toolchange's
// entry (tcr.start_pos): route the approach around the tower's bounding box so the
// nozzle enters through that opening instead of dragging across the printed wall
// (append_tcr parity). Emits only the waypoints leading up to the opening — the
// caller still travels to start_wipe_pos itself. Returns an empty string when the
// gap wall is off (option off or cone wall) or the approach already starts inside
// the tower: such hops never cross the wall and must stay direct.
std::string WipeTowerIntegration::travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const
{
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
return {};
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
// Transform the tower-local bbx corners exactly like the tcr points; a rotated
// tower gets a conservative axis-aligned envelope.
Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon();
for (auto& p : avoid_points.points)
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
BoundingBox avoid_bbx(avoid_points.points);
if (avoid_bbx.contains(route_start))
return {};
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen));
std::string gcode;
// The polyline's last point is start_wipe_pos itself — emitted by the caller.
for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i)
gcode += gcodegen.travel_to(travel_polyline.points[i], erMixed, "Travel to a Wipe Tower");
return gcode;
}
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
{
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
@@ -1001,6 +1061,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string change_filament_gcode = gcodegen.config().change_filament_gcode.value;
bool is_used_travel_avoid_perimeter = gcodegen.m_config.prime_tower_skip_points.value;
if (is_nozzle_change && !tcr.nozzle_change_result.is_extruder_change) is_used_travel_avoid_perimeter = false;
// add nozzle change gcode into change filament gcode
std::string nozzle_change_gcode_trans;
@@ -1263,24 +1324,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]};
Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast<double>() + plate_origin_2d.cast<double>());
Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d);
BoundingBox avoid_bbx, printer_bbx;
{
// set printer_bbx
// Multi-nozzle: clamp the avoid-perimeter travel bounds to the region every
// extruder can reach (get_extruder_shared_printable_polygon) instead of the full
// bed. Gated on the multi-nozzle predicate so H2D and every existing single/dual
// printer keep the historic full-printable_area routing byte-identical.
if (is_multi_nozzle_printer(gcodegen.m_config)) {
printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon());
printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d);
printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d);
} else {
Pointfs bed_pointsf = gcodegen.m_config.printable_area.values;
Points bed_points;
for (auto p : bed_pointsf) { bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d)); }
printer_bbx = BoundingBox(bed_points);
}
}
BoundingBox avoid_bbx, printer_bbx = printer_travel_bounds(gcodegen);
{
// set avoid_bbx
avoid_bbx = scaled(m_wipe_tower_bbx);
@@ -1310,20 +1354,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
}
// do unretract after setting current extruder_id
// PETG filaments on a device with a filament switcher get a small (2 mm) pre-extrusion
// before the tool change. has_filament_switcher is a develop-only key read defensively from the
// full config (Orca does not carry it as a static PrintConfig member — same convention as
// enable_filament_dynamic_map); no shipping profile sets it (grep resources/profiles = 0), so
// is_petg_pre_extrusion is always false -> extra_unretract stays 0 -> byte-identical to the plain
// unretract() fleet-wide. The tower-interface contact pre-extrusion length (the
// is_contact_pre_extrusion branch) is NOT applied here; it is only computed as the guard used to
// give the contact path priority over PETG.
// BBS pattern: the wipe tower shifts the toolchange start position outward for the
// tower-interface (contact) pre-extrusion and for the PETG-with-filament-switcher case;
// the pre-extrusion material itself is laid down here as extra unretract on the approach.
// has_filament_switcher is a develop-only key read defensively from the full config (Orca
// does not carry it as a static PrintConfig member — same convention as
// enable_filament_dynamic_map); no shipping profile sets it, so is_petg_pre_extrusion is
// always false fleet-wide.
const ConfigOptionBool* has_filament_switcher_opt = gcodegen.m_print->full_print_config().option<ConfigOptionBool>("has_filament_switcher");
bool is_contact_pre_extrusion = tcr.is_contact && gcodegen.m_config.enable_tower_interface_features;
bool is_petg_pre_extrusion = !is_contact_pre_extrusion
&& gcodegen.config().filament_type.get_at(tcr.new_tool) == "PETG"
&& has_filament_switcher_opt && has_filament_switcher_opt->value;
float extra_unretract = is_petg_pre_extrusion ? 2.f : 0.f;
float extra_unretract = 0.f;
if (is_contact_pre_extrusion)
extra_unretract = gcodegen.m_config.filament_tower_interface_pre_extrusion_length.get_at(tcr.new_tool);
else if (is_petg_pre_extrusion)
extra_unretract = 2.f;
std::string toolchange_unretract_str = (extra_unretract > 0.f) ? gcodegen.unretract(extra_unretract) : gcodegen.unretract();
check_add_eol(toolchange_unretract_str);
@@ -1421,20 +1468,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position
float alpha = m_wipe_tower_rotation / 180.f * float(M_PI);
auto transform_wt_pt = [&alpha, this](const Vec2f &pt) -> Vec2f {
Vec2f out = Eigen::Rotation2Df(alpha) * pt;
out += m_wipe_tower_pos;
return out;
};
// Priming lines are absolute bed moves; everything else is tower-local
// (transform_wt2_pt).
Vec2f start_pos = tcr.start_pos;
Vec2f end_pos = tcr.end_pos;
if (!tcr.priming) {
start_pos = transform_wt_pt(start_pos);
end_pos = transform_wt_pt(end_pos);
start_pos = transform_wt2_pt(start_pos);
end_pos = transform_wt2_pt(end_pos);
}
Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos;
Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : Vec2f(m_wipe_tower_pos + Eigen::Rotation2Df(alpha) * m_rib_offset);
float wipe_tower_rotation = tcr.priming ? 0.f : alpha;
Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
@@ -1464,16 +1507,34 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|| is_ramming
|| tool_change_on_wipe_tower);
if (should_travel_to_tower || gcodegen.m_need_change_layer_lift_z) {
const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d);
const bool travel_to_tower_now = should_travel_to_tower || gcodegen.m_need_change_layer_lift_z;
if (travel_to_tower_now) {
// FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges,
// then we could simplify the condition and make it more readable.
gcode += gcodegen.retract();
// Orca: pass the configured lift type, as append_tcr does above. lazy_lift() keeps
// the first type it is given, so the NormalLift default would pin this hop to a
// standing move. Slope and spiral both need a known head position.
LiftType lift_type = LiftType::NormalLift;
if (gcodegen.writer().filament() != nullptr && gcodegen.writer().is_current_position_clear()) {
ZHopType z_hop_type = ZHopType(gcodegen.config().z_hop_types.get_at(
gcodegen.get_filament_config_index((int) gcodegen.writer().filament()->id())));
if (z_hop_type == ZHopType::zhtAuto)
z_hop_type = ZHopType::zhtSpiral;
lift_type = gcodegen.to_lift_type(z_hop_type);
}
gcode += gcodegen.retract(false, false, lift_type);
gcodegen.m_avoid_crossing_perimeters.use_external_mp_once();
gcode += gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d), erMixed, "Travel to a Wipe Tower");
if (!tcr.priming && gcodegen.last_pos_defined())
gcode += travel_to_tower_gap(gcodegen, gcodegen.last_pos(), start_wipe_pos);
gcode += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower");
gcode += gcodegen.unretract();
} else {
// When this is multiextruder printer without any ramming, we can just change
// the tool without travelling to the tower.
// the tool without travelling to the tower. The tower entry travel then lives
// inside the tcr gcode; with skip points on it is rerouted below, once the
// toolchange gcode (and the head position it ends at) is known.
}
if (will_go_down) {
@@ -1496,6 +1557,36 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
toolchange_temp_override = interface_temp;
}
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z
if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) {
// The tool changed in place (multi-tool printer without ramming), so the
// tower entry is the tcr's own positioning move — a straight line across
// the printed wall. Route it around the tower and in through the wall
// opening instead, riding at the end of the change_filament_gcode
// substitution so the generator's positioning move degrades to a
// zero-length one (append_tcr parity: travel after the filament change,
// retracted, with the new filament).
Vec3f last_gcode_pos = gcodegen.writer().get_position().cast<float>();
Point route_start;
bool have_start = false;
if (GCodeProcessor::get_last_position_from_gcode(toolchange_gcode_str, last_gcode_pos)) {
// A custom change_filament_gcode may have moved the head (tool docks
// etc.); recover the real position from the emitted gcode.
route_start = gcodegen.gcode_to_point(Vec2d(last_gcode_pos.x(), last_gcode_pos.y()) + plate_origin_2d.cast<double>());
have_start = true;
} else if (gcodegen.last_pos_defined()) {
route_start = gcodegen.last_pos();
have_start = true;
}
if (have_start) {
gcodegen.set_last_pos(route_start);
gcodegen.m_avoid_crossing_perimeters.use_external_mp_once();
std::string travel = travel_to_tower_gap(gcodegen, route_start, start_wipe_pos);
travel += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower");
check_add_eol(travel);
toolchange_gcode_str += travel;
gcodegen.set_last_pos(start_wipe_pos);
}
}
if (gcodegen.config().enable_prime_tower) {
deretraction_str += gcodegen.writer().travel_to_z(z, "Force restore layer Z", true);
Vec3d position{gcodegen.writer().get_position()};
@@ -1681,7 +1772,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// Prepare a future wipe.
gcodegen.m_wipe.reset_path();
for (const Vec2f& wipe_pt : tcr.wipe_path)
gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(wipe_pt) + plate_origin_2d));
gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(wipe_pt) + plate_origin_2d));
}
// Let the planner know we are traveling between objects.
+3
View File
@@ -132,6 +132,9 @@ private:
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const;
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
Vec2f transform_wt2_pt(const Vec2f &pt) const;
BoundingBox printer_travel_bounds(GCode &gcodegen) const;
// Postprocesses gcode: rotates and moves G1 extrusions and returns result
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
+2 -2
View File
@@ -1450,8 +1450,8 @@ void GCodeProcessor::run_post_process()
// flag) runs none of this. It is pure data construction — it only fills m_filament_blocks /
// m_extruder_blocks / m_machine_*_gcode_*_line_id and never touches the exported g-code, so even
// the enable_pre_heating fleet stays byte-identical (nothing reads the blocks until the injection
// pass). In practice it also stays empty/degenerate today because no template/code yet emits the
// MACHINE_*_GCODE_* / NOZZLE_CHANGE_* / CP_TOOLCHANGE_WIPE markers it keys off.
// pass). The wipe tower emits the NOZZLE_CHANGE_* (ramming) and CP_TOOLCHANGE_WIPE markers this
// builder keys off; the MACHINE_*_GCODE_* markers come from the machine g-code templates.
m_filament_blocks.clear();
m_extruder_blocks.clear();
m_machine_start_gcode_end_line_id = (unsigned int) (-1);
+2 -1
View File
@@ -143,7 +143,8 @@ BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_
double wipe_tower_y = print.config().wipe_tower_y.get_at(plate_idx) + plate_origin(1);
Transform2d trafo =
Eigen::Translation2d(wipe_tower_x, wipe_tower_y) *
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value));
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value)) *
Eigen::Translation2d(print.wipe_tower_data().rib_offset.cast<double>()); // tower-local rib-wall shift, zero unless rib
BoundingBoxf bbox;
for (const std::vector<WipeTower::ToolChangeResult> &tool_changes : print.wipe_tower_data().tool_changes) {
File diff suppressed because it is too large Load Diff
+112 -112
View File
@@ -12,7 +12,7 @@
#include "libslic3r/Polyline.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <unordered_set>
#include "libslic3r/MultiNozzleUtils.hpp"
namespace Slic3r
{
@@ -20,6 +20,11 @@ class WipeTowerWriter;
class PrintConfig;
enum GCodeFlavor : unsigned char;
// Cuts the tower wall polygon open at each skip point (a toolchange's entry position)
// so the entry travel can pass through instead of crossing the printed wall. Defined in
// WipeTower.cpp, shared by WipeTower and WipeTower2.
Polylines construct_gap_for_skip_points(
const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon);
class WipeTower
{
@@ -84,7 +89,6 @@ public:
bool priming;
bool is_tool_change{false};
bool is_contact{false};
Vec2f tool_change_start_pos;
// Pass a polyline so that normal G-code generator can do a wipe for us.
@@ -108,6 +112,7 @@ public:
// executing the gcode finish_layer_tcr.
bool is_finish_first = false;
bool is_contact = false;
NozzleChangeResult nozzle_change_result;
// Sum the total length of the extrusion.
@@ -122,6 +127,8 @@ public:
}
return e_length;
}
// Orca: set by WipeTower2 (non-BBL tower) to force a travel to the tower even when the
// previous position is unknown; read by WipeTowerIntegration::append_tcr2 (GCode.cpp).
bool force_travel = false;
};
@@ -162,15 +169,12 @@ public:
bool priming,
size_t old_tool,
bool is_finish,
bool is_tool_change,
float purge_volume,
bool is_contact = false) const;
bool is_tool_change, float purge_volume, bool is_contact) const;
ToolChangeResult construct_block_tcr(WipeTowerWriter& writer,
bool priming,
size_t filament_id,
bool is_finish,
float purge_volume) const;
bool is_finish, float purge_volume) const;
// x -- x coordinates of wipe tower in mm ( left bottom corner )
@@ -184,9 +188,14 @@ public:
// Set the extruder properties.
void set_extruder(size_t idx, const PrintConfig& config);
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
// Orca: has_filament_switcher is not a static PrintConfig member here, so it is pushed in from
// Print via a setter rather than read in the ctor. Device-set only.
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
// Appends into internal structure m_plan containing info about the future wipe tower
// to be used before building begins. The entries must be added ordered in z.
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f, float prime_volume = 0.f);
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume_ec = 0.f, float wipe_volume_nc = 0.f, float prime_volume = 0.f);
// Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result"
void generate(std::vector<std::vector<ToolChangeResult>> &result);
@@ -219,9 +228,6 @@ public:
}
}
void set_wipe_volume(std::vector<std::vector<float>>& wiping_matrix) {
wipe_volumes = wiping_matrix;
}
// Switch to a next layer.
void set_layer(
@@ -250,7 +256,6 @@ public:
// Calculate extrusion flow from desired line width, nozzle diameter, filament diameter and layer_height:
m_extrusion_flow = extrusion_flow(layer_height);
// Advance m_layer_info iterator, making sure we got it right
while (!m_plan.empty() && m_layer_info->z < print_z - WT_EPSILON && m_layer_info+1 != m_plan.end())
++m_layer_info;
@@ -309,20 +314,9 @@ public:
std::vector<float> get_used_filament() const { return m_used_filament_length; }
int get_number_of_toolchanges() const { return m_num_tool_changes; }
void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; }
// Vortek H2C: filament_id → physical nozzle_id for carousel rotation detection
void set_filament_nozzle_map(const std::vector<int> &nozzle_map) { m_filament_nozzle_map = nozzle_map; }
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
bool has_tpu_filament() const { return m_has_tpu_filament; }
// Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print
// via a setter rather than read in the ctor. Device-set only.
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
// The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the
// printable bed.
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
struct FilamentParameters {
std::string material = "PLA";
int category;
@@ -331,15 +325,15 @@ public:
bool is_support = false;
int nozzle_temperature = 0;
int nozzle_temperature_initial_layer = 0;
int interface_print_temperature = 0;
float loading_speed = 0.f;
float loading_speed_start = 0.f;
float unloading_speed = 0.f;
float unloading_speed_start = 0.f;
float delay = 0.f ;
int cooling_moves = 0;
float cooling_initial_speed = 0.f;
float cooling_final_speed = 0.f;
// BBS: remove useless config
//float loading_speed = 0.f;
//float loading_speed_start = 0.f;
//float unloading_speed = 0.f;
//float unloading_speed_start = 0.f;
//float delay = 0.f ;
//int cooling_moves = 0;
//float cooling_initial_speed = 0.f;
//float cooling_final_speed = 0.f;
float ramming_line_width_multiplicator = 1.f;
float ramming_step_multiplicator = 1.f;
float max_e_speed = std::numeric_limits<float>::max();
@@ -349,41 +343,41 @@ public:
float retract_length;
float retract_speed;
float wipe_dist;
float tower_interface_pre_extrusion_dist = 0.f;
float tower_interface_pre_extrusion_length = 0.f;
// Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices;
// set from filament_tower_interface_pre_extrusion_dist.
float petg_pre_extrusion_offset_dist = 0.f;
float tower_ironing_area = 4.f;
float tower_interface_purge_length = 0.f;
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
float filament_cooling_before_tower = 0.f;
// .first = extruder change, .second = nozzle change (carousel)
std::pair<float,float> max_e_ramming_speed{0.f, 0.f};
std::pair<float,float> ramming_travel_time{0.f, 0.f};
std::pair<int,int> precool_target_temp{0, 0};
std::pair<std::vector<float>,std::vector<float>> precool_t;
std::pair<std::vector<float>,std::vector<float>> precool_t_first_layer;
std::pair<float,float> max_e_ramming_speed;//[0]extruder change [1]nozzle change
std::pair<float, float> ramming_travel_time; // Travel time after ramming
std::pair<std::vector<float>,std::vector<float>> precool_t;//Pre-cooling time, set to 0 to ensure the ramming speed is controlled solely by ramming volumetric speed.
std::pair<std::vector<float>, std::vector<float>> precool_t_first_layer;
std::pair<int,int> precool_target_temp;
float filament_cooling_before_tower = 0.f;
float flat_iron_area;
float filament_tower_interface_print_temp;
float filament_tower_interface_pre_extrusion_dist = 0;
float filament_tower_interface_pre_extrusion_length = 0;
float filament_petg_pre_extrusion_offset_dist = 0;
};
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
void set_filament_categories(const std::vector<int> & filament_categories) { m_filament_categories = filament_categories;};
std::vector<int> m_used_filament_ids;
void set_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult &multi_nozzle_group_result) { m_multi_nozzle_group_result = &multi_nozzle_group_result; };
std::vector<int> m_used_filament_ids;
std::vector<int> m_filament_categories;
const MultiNozzleUtils::LayeredNozzleGroupResult *m_multi_nozzle_group_result{nullptr};
enum class WipeTowerLayerType : unsigned char { Normal, Contact, Solid, Contact_UP};// Contact layer should be solid and reduce feed
struct WipeTowerBlock
{
int block_id{0};
int filament_adhesiveness_category{0};
std::vector<float> layer_depths;
std::vector<bool> solid_infill;
//std::vector<bool> solid_infill;
std::vector<float> finish_depth{0}; // the start pos of finish frame for every layer
std::vector<WipeTowerLayerType> layers_type; // type of the layer, normal, Contact or Solid
float depth{0};
float start_depth{0};
float cur_depth{0};
int last_filament_change_id{-1};
int last_filament_change_id{-1};
int last_nozzle_change_id{-1};
};
@@ -403,25 +397,33 @@ public:
WipeTowerBlock* get_block_by_category(int filament_adhesiveness_category, bool create);
void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false);
int get_filament_category(int filament_id);
bool is_in_same_extruder(int filament_id_1, int filament_id_2);
// Vortek H2C: format BBS-compatible NOZZLE_CHANGE_START/END tag with OF/NF/ON/NN payload
std::string format_nozzle_change_tag(bool start, int old_filament_id, int new_filament_id) const;
void reset_block_status();
int get_wall_filament_for_all_layer();
// for generate new wipe tower
void generate_new(std::vector<std::vector<WipeTower::ToolChangeResult>> &result);
void plan_tower_new();
void generate_wipe_tower_blocks();
void generate_wipe_tower_blocks(bool add_solid_flag);
void update_all_layer_depth(float wipe_tower_depth);
void set_nozzle_last_layer_id();
void set_first_layer_flow_ratio(const float flow_ratio);
// Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig
// members in BBS), so Print pushes the resolved per-variant columns in via this setter.
void set_accelerations(const std::vector<double> &normal, const std::vector<double> &first_layer_normal,
const std::vector<double> &travel, const std::vector<double> &first_layer_travel);
void calc_block_infill_gap();
ToolChangeResult tool_change_new(size_t new_tool, bool solid_change = false, bool solid_nozzlechange=false);
NozzleChangeResult nozzle_change_new(int old_filament_id, int new_filament_id, bool solid_change = false);
NozzleChangeResult ramming(int old_filament_id, int new_filament_id, bool solid_change = false, bool extruder_change = true); // extruder_chang means nozzle_change
ToolChangeResult finish_layer_new(bool extrude_perimeter = true, bool extrude_fill = true, bool extrude_fill_wall = true);
ToolChangeResult finish_block(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true);
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true ,bool interface_solid =false);
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true, WipeTowerLayerType layer_type = WipeTowerLayerType::Normal);
void toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_length,bool solid_toolchange=false);
Vec2f get_rib_offset() const { return m_rib_offset; }
bool is_need_ramming(int filament_id_1, int filament_id_2, int layer_id) const;
bool is_same_extruder(int filament_id_1, int filament_id_2, int layer_id) const;
bool is_same_nozzle(int filament_id_1, int filament_id_2, int layer_id) const;
int get_nozzle_id(int filament_id, int layer_id) const;
int get_extruder_id(int filament_id, int layer_id) const;
private:
enum wipe_shape // A fill-in direction
@@ -441,7 +443,6 @@ private:
bool m_enable_wrapping_detection = false;
bool m_enable_timelapse_print = false;
bool m_semm = true; // Are we using a single extruder multimaterial printer?
bool m_purge_in_prime_tower = false; // Do we purge in the prime tower?
Vec2f m_wipe_tower_pos; // Left front corner of the wipe tower in mm.
float m_wipe_tower_width; // Width of the wipe tower.
float m_wipe_tower_depth = 0.f; // Depth of the wipe tower
@@ -459,12 +460,11 @@ private:
float m_travel_speed = 0.f;
float m_first_layer_speed = 0.f;
size_t m_first_layer_idx = size_t(-1);
std::vector<double> m_filaments_change_length;
Vec2f m_origin;
std::vector<int> m_last_layer_id;
std::pair<std::vector<double>,std::vector<double>> m_filaments_change_length;//[0]extruder change [1]nozzle change
size_t m_cur_layer_id;
NozzleChangeResult m_nozzle_change_result;
std::vector<int> m_filament_map;
std::vector<int> m_filament_nozzle_map; // Vortek H2C: filament_id → physical nozzle_id
bool m_has_tpu_filament{false};
bool m_is_multi_extruder{false};
bool m_use_gap_wall{false};
@@ -475,33 +475,32 @@ private:
bool m_used_fillet{false};
Vec2f m_rib_offset{Vec2f(0.f, 0.f)};
bool m_tower_framework{false};
bool m_need_reverse_travel{false};
bool m_enable_tower_interface_features{false};
// G-code generator parameters.
float m_cooling_tube_retraction = 0.f;
float m_cooling_tube_length = 0.f;
float m_parking_pos_retraction = 0.f;
float m_extra_loading_move = 0.f;
// BBS: remove useless config
//float m_cooling_tube_retraction = 0.f;
//float m_cooling_tube_length = 0.f;
//float m_parking_pos_retraction = 0.f;
//float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_set_extruder_trimpot = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
// Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole
// feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count
// defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged.
bool m_is_multiple_nozzle = false;
std::vector<double> m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder)
std::vector<int> m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param)
// Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height
// (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id
// records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on
// m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a
// multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable
// height (near the Z limit).
std::vector<double> m_printable_height;
std::vector<int> m_last_layer_id;
bool m_is_multiple_nozzle = false;
std::vector<unsigned int> m_normal_accels;
std::vector<unsigned int> m_first_layer_normal_accels;
std::vector<unsigned int> m_travel_accels;
std::vector<unsigned int> m_first_layer_travel_accels;
unsigned int m_max_accels;
bool m_accel_to_decel_enable;
float m_accel_to_decel_factor;
bool m_enable_arc_fitting = true;
std::vector<double> m_hotend_heating_rate;
std::vector<double> m_hotend_cooling_rate;
Polygons m_shared_print_bed;
// Bed properties
enum {
@@ -512,10 +511,11 @@ private:
float m_bed_width; // width of the bed bounding box
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
float m_first_layer_flow_ratio;
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
float m_nozzle_change_perimeter_width = 0.4f * Width_To_Nozzle_Ratio;
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
std::unordered_map<int, std::pair<float,float>> m_block_infill_gap_width; // categories to infill_gap: toolchange gap, nozzlechange gap
// Extruder specific parameters.
std::vector<FilamentParameters> m_filpar;
@@ -528,50 +528,52 @@ private:
// A fill-in direction (positive Y, negative Y) alternates with each layer.
wipe_shape m_current_shape = SHAPE_NORMAL;
size_t m_current_tool = 0;
// Orca: support mmu wipe tower
std::vector<std::vector<float>> wipe_volumes;
// BBS
//const std::vector<std::vector<float>> wipe_volumes;
float m_depth_traversed = 0.f; // Current y position at the wipe tower.
bool m_current_layer_finished = false;
bool m_left_to_right = true;
float m_extra_spacing = 1.f;
float m_tpu_fixed_spacing = 2;
std::vector<Vec2f> m_wall_skip_points;
float m_max_speed = 5400.f; // the maximum printing speed on the prime tower.
std::vector<std::vector<Vec2f>> m_wall_skip_points;
std::map<float,Polylines> m_outer_wall;
std::vector<double> m_printable_height;
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
bool m_flat_ironing=false;
bool m_enable_tower_interface_features=false;
bool m_enable_tower_interface_cooldown_during_tower=false;
// Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset.
// m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so
// the PETG branch in get_next_pos never runs -> no change fleet-wide.
bool m_has_filament_switcher=false;
Polygons m_shared_print_bed;
bool m_prev_layer_had_interface=false;
bool m_current_layer_has_interface=false;
bool m_contact_ironing = false;
bool m_has_filament_switcher = false;
float m_contact_speed = 20 * 60.f;
std::vector<int> m_physical_extruder_map;
// Calculates length of extrusion line to extrude given volume
float volume_to_length(float volume, float line_width, float layer_height) const {
return std::max(0.f, volume / (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
}
// Calculates volume of extrusion line
float length_to_volume(float length,float line_width, float layer_height) const
{
return std::max(0.f, length * (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
}
// Calculates depth for all layers and propagates them downwards
void plan_tower();
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
void make_wipe_tower_square();
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool interface_layer, size_t interface_tool);
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool solid_toolchange);
// Goes through m_plan, calculates border and finish_layer extrusions and subtracts them from last wipe
void save_on_last_wipe();
bool is_tpu_filament(int filament_id) const;
bool is_petg_filament(int filament_id) const;
bool is_need_reverse_travel(int filament_id, bool extruder_change) const;
bool is_need_reverse_travel(int filament, bool extruder_change) const;
// BBS
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
void set_for_wipe_tower_writer(WipeTowerWriter &writer);
// to store information about tool changes for a given layer
struct WipeTowerInfo{
@@ -584,6 +586,7 @@ private:
float wipe_volume;
float wipe_length;
float nozzle_change_depth{0};
float nozzle_change_length{0};
// BBS
float purge_volume;
ToolChange(size_t old, size_t newtool, float depth=0.f, float ramming_depth=0.f, float fwl=0.f, float wv=0.f, float wl = 0, float pv = 0)
@@ -613,7 +616,7 @@ private:
// ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble_nonsupport(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
WipeTowerInfo::ToolChange set_toolchange(int old_tool, int new_tool, float layer_height, float wipe_volume, float purge_volume,int layer_id);
void toolchange_Unload(
WipeTowerWriter &writer,
const box_coordinates &cleaning_box,
@@ -633,13 +636,10 @@ private:
WipeTowerWriter &writer,
const box_coordinates &cleaning_box,
float wipe_volume);
void get_wall_skip_points(const WipeTowerInfo &layer);
// Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns
// false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that
// extruder's printable height; returns true (no clamp) in every other case.
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
void set_nozzle_last_layer_id();
void get_wall_skip_points(const WipeTowerInfo &layer,int layer_id);
void get_all_wall_skip_points();
ToolChangeResult merge_tcr(ToolChangeResult &first, ToolChangeResult &second);
float get_block_gap_width(int tool, bool is_nozzlechangle = false);
};
+260 -321
View File
@@ -24,7 +24,6 @@
namespace Slic3r
{
static constexpr float flat_iron_area = 4.f;
constexpr float flat_iron_speed = 10.f * 60.f;
static const double wipe_tower_wall_infill_overlap = 0.0;
static constexpr double WIPE_TOWER_RESOLUTION = 0.1;
@@ -234,24 +233,6 @@ static Polygon rounding_rectangle(Polygon& polygon, double rounding = 2., double
return res;
}
static std::pair<bool, Vec2f> ray_intersetion_line(const Vec2f& a, const Vec2f& v1, const Vec2f& b, const Vec2f& c)
{
const Vec2f v2 = c - b;
double denom = cross2(v1, v2);
if (fabs(denom) < EPSILON)
return {false, Vec2f(0, 0)};
const Vec2f v12 = (a - b);
double nume_a = cross2(v2, v12);
double nume_b = cross2(v1, v12);
double t1 = nume_a / denom;
double t2 = nume_b / denom;
if (t1 >= 0 && t2 >= 0 && t2 <= 1.) {
// Get the intersection point.
Vec2f res = a + t1 * v1;
return std::pair<bool, Vec2f>(true, res);
}
return std::pair<bool, Vec2f>(false, Vec2f{0, 0});
}
static Polygon scale_polygon(const std::vector<Vec2f>& points)
{
Polygon res;
@@ -296,6 +277,7 @@ static Polygon generate_rectange(const Line& line, coord_t offset)
return poly;
};
// Straight or arc-fitted wall segment used by WipeTowerWriter2::generate_path().
struct Segment
{
Vec2f start;
@@ -306,234 +288,6 @@ struct Segment
bool is_valid() const { return start.y() < end.y(); }
};
static std::vector<Segment> remove_points_from_segment(const Segment& segment, const std::vector<Vec2f>& skip_points, double range)
{
std::vector<Segment> result;
result.push_back(segment);
float x = segment.start.x();
for (const Vec2f& point : skip_points) {
std::vector<Segment> newResult;
for (const auto& seg : result) {
if (point.y() + range <= seg.start.y() || point.y() - range >= seg.end.y()) {
newResult.push_back(seg);
} else {
if (point.y() - range > seg.start.y()) {
newResult.push_back(Segment(Vec2f(x, seg.start.y()), Vec2f(x, point.y() - range)));
}
if (point.y() + range < seg.end.y()) {
newResult.push_back(Segment(Vec2f(x, point.y() + range), Vec2f(x, seg.end.y())));
}
}
}
result = newResult;
}
result.erase(std::remove_if(result.begin(), result.end(), [](const Segment& seg) { return !seg.is_valid(); }), result.end());
return result;
}
struct IntersectionInfo
{
Vec2f pos;
int idx;
int pair_idx; // gap_pair idx
float dis_from_idx;
bool is_forward;
};
struct PointWithFlag
{
Vec2f pos;
int pair_idx; // gap_pair idx
bool is_forward;
};
static IntersectionInfo move_point_along_polygon(
const std::vector<Vec2f>& points, const Vec2f& startPoint, int startIdx, float offset, bool forward, int pair_idx)
{
float remainingDistance = offset;
IntersectionInfo res;
int mod = points.size();
if (forward) {
int next = (startIdx + 1) % mod;
remainingDistance -= (points[next] - startPoint).norm();
if (remainingDistance <= 0) {
res.idx = startIdx;
res.pos = startPoint + (points[next] - startPoint).normalized() * offset;
res.pair_idx = pair_idx;
res.dis_from_idx = (points[startIdx] - res.pos).norm();
return res;
} else {
for (int i = (startIdx + 1) % mod; i != startIdx; i = (i + 1) % mod) {
float segmentLength = (points[(i + 1) % mod] - points[i]).norm();
if (remainingDistance <= segmentLength) {
float ratio = remainingDistance / segmentLength;
res.idx = i;
res.pos = points[i] + ratio * (points[(i + 1) % mod] - points[i]);
res.dis_from_idx = remainingDistance;
res.pair_idx = pair_idx;
return res;
}
remainingDistance -= segmentLength;
}
res.idx = (startIdx - 1 + mod) % mod;
res.pos = points[startIdx];
res.pair_idx = pair_idx;
res.dis_from_idx = (res.pos - points[res.idx]).norm();
}
} else {
int next = (startIdx + 1) % mod;
remainingDistance -= (points[startIdx] - startPoint).norm();
if (remainingDistance <= 0) {
res.idx = startIdx;
res.pos = startPoint - (points[next] - points[startIdx]).normalized() * offset;
res.dis_from_idx = (res.pos - points[startIdx]).norm();
res.pair_idx = pair_idx;
return res;
}
for (int i = (startIdx - 1 + mod) % mod; i != startIdx; i = (i - 1 + mod) % mod) {
float segmentLength = (points[(i + 1) % mod] - points[i]).norm();
if (remainingDistance <= segmentLength) {
float ratio = remainingDistance / segmentLength;
res.idx = i;
res.pos = points[(i + 1) % mod] - ratio * (points[(i + 1) % mod] - points[i]);
res.dis_from_idx = segmentLength - remainingDistance;
res.pair_idx = pair_idx;
return res;
}
remainingDistance -= segmentLength;
}
res.idx = startIdx;
res.pos = points[res.idx];
res.pair_idx = pair_idx;
res.dis_from_idx = 0;
}
return res;
};
static void insert_points(std::vector<PointWithFlag>& pl, int idx, Vec2f pos, int pair_idx, bool is_forward)
{
int next = (idx + 1) % pl.size();
Vec2f pos1 = pl[idx].pos;
Vec2f pos2 = pl[next].pos;
if ((pos - pos1).squaredNorm() < EPSILON) {
pl[idx].pair_idx = pair_idx;
pl[idx].is_forward = is_forward;
} else if ((pos - pos2).squaredNorm() < EPSILON) {
pl[next].pair_idx = pair_idx;
pl[next].is_forward = is_forward;
} else {
pl.insert(pl.begin() + idx + 1, PointWithFlag{pos, pair_idx, is_forward});
}
}
static Polylines remove_points_from_polygon(
const Polygon& polygon, const std::vector<Vec2f>& skip_points, double range, bool is_left, Polygon& insert_skip_pg)
{
assert(polygon.size() > 2);
Polylines result;
std::vector<PointWithFlag> new_pl; // add intersection points for gaps, where bool indicates whether it's a gap point.
std::vector<IntersectionInfo> inter_info;
Vec2f ray = is_left ? Vec2f(-1, 0) : Vec2f(1, 0);
auto polygon_box = get_extents(polygon);
Point anchor_point = is_left ? Point{polygon_box.max[0], polygon_box.min[1]} : polygon_box.min; // rd:ld
std::vector<Vec2f> points;
{
points.reserve(polygon.points.size());
int idx = polygon.closest_point_index(anchor_point);
Polyline tmp_poly = polygon.split_at_index(idx);
for (auto& p : tmp_poly)
points.push_back(unscale(p).cast<float>());
points.pop_back();
}
for (int i = 0; i < skip_points.size(); i++) {
for (int j = 0; j < points.size(); j++) {
Vec2f& p1 = points[j];
Vec2f& p2 = points[(j + 1) % points.size()];
auto [is_inter, inter_pos] = ray_intersetion_line(skip_points[i], ray, p1, p2);
if (is_inter) {
IntersectionInfo forward = move_point_along_polygon(points, inter_pos, j, range, true, i);
IntersectionInfo backward = move_point_along_polygon(points, inter_pos, j, range, false, i);
backward.is_forward = false;
forward.is_forward = true;
inter_info.push_back(backward);
inter_info.push_back(forward);
break;
}
}
}
// insert point to new_pl
for (const auto& p : points)
new_pl.push_back({p, -1});
std::sort(inter_info.begin(), inter_info.end(), [](const IntersectionInfo& lhs, const IntersectionInfo& rhs) {
if (rhs.idx == lhs.idx)
return lhs.dis_from_idx < rhs.dis_from_idx;
return lhs.idx < rhs.idx;
});
for (int i = inter_info.size() - 1; i >= 0; i--) {
insert_points(new_pl, inter_info[i].idx, inter_info[i].pos, inter_info[i].pair_idx, inter_info[i].is_forward);
}
{
// set insert_pg for wipe_path
for (auto& p : new_pl)
insert_skip_pg.points.push_back(scaled(p.pos));
}
int beg = 0;
bool skip = true;
int i = beg;
Polyline pl;
do {
if (skip || new_pl[i].pair_idx == -1) {
pl.points.push_back(scaled(new_pl[i].pos));
i = (i + 1) % new_pl.size();
skip = false;
} else {
if (!pl.points.empty()) {
pl.points.push_back(scaled(new_pl[i].pos));
result.push_back(pl);
pl.points.clear();
}
int left = new_pl[i].pair_idx;
int j = (i + 1) % new_pl.size();
while (j != beg && new_pl[j].pair_idx != left) {
if (new_pl[j].pair_idx != -1 && !new_pl[j].is_forward)
left = new_pl[j].pair_idx;
j = (j + 1) % new_pl.size();
}
i = j;
skip = true;
}
} while (i != beg);
if (!pl.points.empty()) {
if (new_pl[i].pair_idx == -1)
pl.points.push_back(scaled(new_pl[i].pos));
result.push_back(pl);
}
return result;
}
static Polylines contrust_gap_for_skip_points(
const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon)
{
if (skip_points.empty()) {
insert_skip_polygon = polygon;
return Polylines{to_polyline(polygon)};
}
bool is_left = false;
const auto& pt = skip_points.front();
if (abs(pt.x()) < wt_width / 2.f) {
is_left = true;
}
return remove_points_from_polygon(polygon, skip_points, gap_length, is_left, insert_skip_polygon);
};
static Polygon generate_rectange_polygon(const Vec2f& wt_box_min, const Vec2f& wt_box_max)
{
Polygon res;
@@ -1245,6 +999,12 @@ WipeTower::ToolChangeResult WipeTower2::construct_tcr(WipeTowerWriter2& writer,
bool WipeTower2::use_gap_wall(const PrintConfig& config)
{
// The cone wall has its own fully separate generator with no gap machinery.
return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone;
}
WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) :
m_semm(config.single_extruder_multi_material.value),
m_enable_filament_ramming(config.enable_filament_ramming.value),
@@ -1272,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_rib_width(config.wipe_tower_rib_width),
m_extra_rib_length(config.wipe_tower_extra_rib_length),
m_wall_type((int)config.wipe_tower_wall_type),
m_flat_ironing(config.prime_tower_flat_ironing.value),
m_use_gap_wall(use_gap_wall(config)),
m_enable_tower_interface_features(config.enable_tower_interface_features.value),
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
{
@@ -1342,6 +1102,7 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config)
m_filpar[idx].is_soluble = (idx != size_t(m_wipe_tower_filament - 1));
else
m_filpar[idx].is_soluble = config.filament_soluble.get_at(idx);
m_filpar[idx].is_support = config.filament_is_support.get_at(idx);
m_filpar[idx].temperature = config.nozzle_temperature.get_at(idx);
m_filpar[idx].first_layer_temperature = config.nozzle_temperature_initial_layer.get_at(idx);
m_filpar[idx].filament_minimal_purge_on_wipe_tower = config.filament_minimal_purge_on_wipe_tower.get_at(idx);
@@ -1478,11 +1239,11 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime(
toolchange_Load(writer, cleaning_box); // Prime the tool.
if (idx_tool + 1 == tools.size()) {
// Last tool should not be unloaded, but it should be wiped enough to become of a pure color.
toolchange_Wipe(writer, cleaning_box, wipe_volumes[tools[idx_tool-1]][tool], false);
toolchange_Wipe(writer, cleaning_box, wipe_volumes[tools[idx_tool-1]][tool], false, true);
} else {
// Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool.
//writer.travel(writer.x(), writer.y() + m_perimeter_width, 7200);
toolchange_Wipe(writer, cleaning_box , 20.f, false);
toolchange_Wipe(writer, cleaning_box , 20.f, false, true);
WipeTower::box_coordinates box = cleaning_box;
box.translate(0.f, writer.y() - cleaning_box.ld.y() + m_perimeter_width);
toolchange_Unload(writer, box , m_filpar[m_current_tool].material, m_filpar[m_current_tool].first_layer_temperature, m_filpar[tools[idx_tool + 1]].first_layer_temperature);
@@ -1525,8 +1286,9 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
float wipe_area = 0.f;
float wipe_volume = 0.f;
float ramming_depth = 0.f;
bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface;
// Finds this toolchange info
if (tool != (unsigned int)(-1))
{
@@ -1534,6 +1296,7 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
if ( b.new_tool == tool ) {
wipe_volume = b.wipe_volume;
wipe_area = b.required_depth;
ramming_depth = b.ramming_depth;
break;
}
}
@@ -1571,7 +1334,9 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
writer.speed_override_backup();
writer.speed_override(100);
Vec2f initial_position = cleaning_box.ld + Vec2f(0.f, m_depth_traversed);
// On a boundary wipe start this enters at the wall gap on the first wipe row;
// toolchange_Unload() then climbs back up to the ram band along the box interior.
Vec2f initial_position = toolchange_entry_pos(m_depth_traversed, ramming_depth, is_first_layer());
writer.set_initial_position(initial_position, m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation);
// Increase the extruder driver current to allow fast ramming.
@@ -1580,6 +1345,11 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
// Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool.
if (tool != (unsigned int)-1){ // This is not the last change.
// Without a ram — or with the boundary wipe start, where the ram band is
// quantized to whole rows — the box is planned as whole wipe rows; the wipe
// then fills it completely so adjacent purge blocks stay contiguous. Uses the
// old tool (m_current_tool before toolchange_Change).
const bool fill_box = !tool_ramming_enabled(m_current_tool) || boundary_wipe_start_enabled(m_current_tool);
auto new_tool_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature;
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material,
(is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature),
@@ -1602,7 +1372,7 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
writer.extrude_explicit(target_x, writer.y(), pre_len, 600.f);
}
}
toolchange_Wipe(writer, cleaning_box, wipe_volume, interface_layer); // Wipe the newly loaded filament until the end of the assigned wipe area.
toolchange_Wipe(writer, cleaning_box, wipe_volume, interface_layer, false, fill_box); // Wipe the newly loaded filament until the end of the assigned wipe area.
if (interface_layer) {
int interface_temp = m_filpar[tool].interface_print_temperature;
if (!m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp)
@@ -1657,11 +1427,20 @@ void WipeTower2::toolchange_Unload(
float remaining = xr - xl ; // keeps track of distance to the next turnaround
float e_done = 0; // measures E move done from each segment
// Orca: Do ramming when SEMM and ramming is enabled or when multi tool head when ramming is enabled on the multi tool.
const bool do_ramming = (m_semm && m_enable_filament_ramming) || m_filpar[m_current_tool].multitool_ramming;
const bool do_ramming = tool_ramming_enabled(m_current_tool);
const bool cold_ramming = m_is_mk4mmu3;
// Orca: see set_toolchange() — quantized ram band + wipe restart at the boundary.
const bool boundary_wipe_start = boundary_wipe_start_enabled(m_current_tool);
float planned_ramming_depth = 0.f;
if (boundary_wipe_start && m_layer_info != m_plan.end())
for (const auto& tch : m_layer_info->tool_changes)
if (tch.old_tool == m_current_tool) { planned_ramming_depth = tch.ramming_depth; break; }
if (do_ramming) {
if (boundary_wipe_start)
// The entry sits at the wall gap on the first wipe row past the reserved
// ram band; step inward first, then move to the band clear of the wall.
writer.travel(Vec2f(ramming_start_pos.x(), writer.y()));
writer.travel(ramming_start_pos); // move to starting position
if (! m_is_mk4mmu3)
writer.disable_linear_advance();
@@ -1672,7 +1451,8 @@ void WipeTower2::toolchange_Unload(
writer.set_position(ramming_start_pos);
// if the ending point of the ram would end up in mid air, align it with the end of the wipe tower:
if (do_ramming && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) {
// (with a boundary wipe start the band is quantized to whole rows below, so no phase alignment is needed)
if (do_ramming && !boundary_wipe_start && (m_layer_info > m_plan.begin() && m_layer_info < m_plan.end() && (m_layer_info-1!=m_plan.begin() || !m_adhesion ))) {
// this is y of the center of previous sparse infill border
float sparse_beginning_y = 0.f;
@@ -1731,6 +1511,28 @@ void WipeTower2::toolchange_Unload(
e_done = 0;
}
}
// Orca: quantize the ram band up to the whole reserved rows (BBL quantizes the
// old-tool purge the same way) so no unprinted void is left between the band and
// the wipe restarting at the boundary below it.
if (planned_ramming_depth > 0.f) {
const int reserved_rows = std::max(1, int(std::round(planned_ramming_depth / y_step)));
const float last_row_y = ramming_start_pos.y() + (reserved_rows - 1) * y_step;
// Same bead model as the ramming segments above: E per mm of ram line.
const float e_per_mm = 1.f / (volume_to_length(1.f, line_width, m_layer_height) * filament_area());
const float fill_feed = m_filpar[m_current_tool].ramming_speed.empty() ? 3000.f :
60.f * volume_to_length(m_filpar[m_current_tool].ramming_speed.back(), line_width, m_layer_height);
while (true) {
const float target_x = m_left_to_right ? xr : xl;
if (std::abs(target_x - writer.x()) > WT_EPSILON)
writer.ram(writer.x(), target_x, 0.f, 0.f, e_per_mm * std::abs(target_x - writer.x()), fill_feed);
if (writer.y() + 0.5f * y_step > last_row_y)
break;
writer.travel(writer.x(), writer.y() + y_step, 7200);
m_left_to_right = !m_left_to_right;
}
}
Vec2f end_of_ramming(writer.x(),writer.y());
writer.change_analyzer_line_width(m_perimeter_width); // so the next lines are not affected by ramming_line_width_multiplier
@@ -1838,10 +1640,27 @@ void WipeTower2::toolchange_Unload(
// this is to align ramming and future wiping extrusions, so the future y-steps can be uniform from the start:
// the perimeter_width will later be subtracted, it is there to not load while moving over just extruded material
Vec2f pos = Vec2f(end_of_ramming.x(), end_of_ramming.y() + (y_step/m_extra_spacing_ramming-m_perimeter_width) / 2.f + m_perimeter_width);
if (do_ramming)
if (planned_ramming_depth > 0.f) {
// Orca: restart the wipe at the left-edge boundary on a fresh row below the
// quantized ram band so the entry scrub always runs at the wall gap (BBL keeps
// CP_TOOLCHANGE_WIPE starting at a box corner the same way). Same lattice
// formula as the no-ram branch below, offset by the ram band.
writer.travel(Vec2f(ramming_start_pos.x(),
cleaning_box.ld.y() + m_depth_traversed +
wipe_start_offset_after_ram(planned_ramming_depth, is_first_layer()) + m_perimeter_width), 2400.f);
m_left_to_right = true;
}
else if (do_ramming)
writer.travel(pos, 2400.f);
else
writer.set_position(pos);
else {
// Orca: with no ram printed there is no ramming geometry to align with. Start the
// first wipe row so the purge row lattice continues across the block boundary
// (previous box's last row top edge sits at its box top): with the planned depth
// of rows * dy, the last row's top edge then lands exactly on this box's top and
// no blank band is left between adjacent purge blocks.
writer.set_position(Vec2f(end_of_ramming.x(),
cleaning_box.ld.y() + m_depth_traversed + wipe_start_offset_after_ram(0.f, is_first_layer()) + m_perimeter_width));
}
writer.resume_preview()
.flush_planner_queue();
@@ -1920,7 +1739,9 @@ void WipeTower2::toolchange_Wipe(
WipeTowerWriter2 &writer,
const WipeTower::box_coordinates &cleaning_box,
float wipe_volume,
bool interface_layer)
bool interface_layer,
bool priming,
bool fill_box)
{
// Increase flow on first layer, slow down print.
writer.set_extrusion_flow(m_extrusion_flow * (is_first_layer() ? 1.18f : 1.f))
@@ -1929,7 +1750,7 @@ void WipeTower2::toolchange_Wipe(
const float& xr = cleaning_box.rd.x();
writer.set_extrusion_flow(m_extrusion_flow * m_extra_flow);
const float line_width = m_perimeter_width * m_extra_flow;
const float line_width = wipe_line_width();
writer.change_analyzer_line_width(line_width);
// Variables x_to_wipe and traversed_x are here to be able to make sure it always wipes at least
@@ -1937,7 +1758,7 @@ void WipeTower2::toolchange_Wipe(
// wipe until the end of the assigned area.
float x_to_wipe = volume_to_length(wipe_volume, m_perimeter_width, m_layer_height) / m_extra_flow;
float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; // Don't use the extra spacing for the first layer, but do use the spacing resulting from increased flow.
float dy = wipe_row_spacing(is_first_layer()); // Don't use the extra spacing for the first layer, but do use the spacing resulting from increased flow.
// All the calculations in all other places take the spacing into account for all the layers.
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
@@ -1950,9 +1771,6 @@ void WipeTower2::toolchange_Wipe(
m_left_to_right = !m_left_to_right;
}
const bool do_ironing = m_flat_ironing && (!interface_layer || !m_enable_tower_interface_features);
const float ironing_area = m_filpar[m_current_tool].tower_ironing_area;
// now the wiping itself:
for (int i = 0; true; ++i) {
if (i!=0) {
@@ -1963,22 +1781,45 @@ void WipeTower2::toolchange_Wipe(
}
float traversed_x = writer.x();
// BBS gap wall: iron the first few mm of the purge, then drag the retracted nozzle
// back out through the wall gap and scrub it with a dry spiral centred on the entry
// point so the toolchange start blob is not left on the wall (same sequence as the
// BBL tower's toolchange_wipe_new; the spiral self-disables when the filament's
// tower ironing area is 0). WT2's entry gap always sits at the left-edge entry
// point, so only iron when the purge actually starts there heading right (in-place
// toolchangers do; SEMM ram/cooling moves leave the nozzle mid-box, far from any gap).
if (i == 0 && m_use_gap_wall && !interface_layer && !priming && m_left_to_right &&
writer.x() - xl < 2.5f * line_width) {
float ironing_length = 3.f;
if (xr - writer.x() < ironing_length)
ironing_length = std::max(xr - writer.x(), 0.f);
const float retract_length = m_filpar[m_current_tool].retract_length;
const float retract_speed = m_filpar[m_current_tool].retract_speed * 60.f;
writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed);
writer.retract(retract_length, retract_speed);
writer.travel(writer.x() - 1.5f * ironing_length, writer.y(), 600.f);
writer.travel(writer.x() + 0.5f * ironing_length, writer.y(), 240.f);
const Vec2f iron_end(writer.x() + ironing_length, writer.y());
writer.spiral_flat_ironing(writer.pos(), m_filpar[m_current_tool].tower_ironing_area, m_perimeter_width, flat_iron_speed);
writer.travel(iron_end, wipe_speed);
writer.retract(-retract_length, retract_speed);
}
if (m_left_to_right)
writer.extrude(xr - (i % 4 == 0 ? 0 : 1.5f*line_width), writer.y(), wipe_speed);
else
writer.extrude(xl + (i % 4 == 1 ? 0 : 1.5f*line_width), writer.y(), wipe_speed);
if (i == 0 && do_ironing && ironing_area > 0.f) {
writer.travel(writer.x(), writer.y(), 600.f);
writer.spiral_flat_ironing(writer.pos(), ironing_area, m_perimeter_width, 10.f * 60.f);
}
if (writer.y()+float(EPSILON) > cleaning_box.lu.y()-0.5f*line_width)
break; // in case next line would not fit
traversed_x -= writer.x();
x_to_wipe -= std::abs(traversed_x);
if (x_to_wipe < WT_EPSILON) {
// Orca: with no ram printed the box was planned as whole wipe rows; fill it
// completely (quantizing the purge up to the planned rows) so the next block
// can start right above it without a blank band in between.
if (!fill_box && x_to_wipe < WT_EPSILON) {
writer.travel(m_left_to_right ? xl + 1.5f*line_width : xr - 1.5f*line_width, writer.y(), 7200);
break;
}
@@ -2110,7 +1951,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
poly = generate_support_cone_wall(writer, wt_box, feedrate, infill_cone, spacing);
} else {
WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width);
poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true, false);
poly = generate_support_rib_wall(writer, wt_box, feedrate, first_layer, m_wall_type == (int)wtwRib, true);
}
// brim (first layer only)
@@ -2228,15 +2069,32 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
return;
// this is an actual toolchange - let's calculate depth to reserve on the wipe tower
float width = m_wipe_tower_width - 3*m_perimeter_width;
float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f),
m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator,
layer_height_par);
// Orca: Set ramming depth to 0 if ramming is disabled.
float ramming_depth = m_enable_filament_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0;
float first_wipe_line = - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width);
const bool first_layer_plan = (m_plan.size() - 1) == m_first_layer_idx;
m_plan.back().tool_changes.push_back(set_toolchange(old_tool, new_tool, layer_height_par, wipe_volume, first_layer_plan));
}
float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par);
WipeTower2::WipeTowerInfo::ToolChange WipeTower2::set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan)
{
float width = m_wipe_tower_width - 3*m_perimeter_width;
float length_to_extrude = volume_to_length((m_semm ? 0.25f : m_filpar[old_tool].multitool_ramming_time) * std::accumulate(m_filpar[old_tool].ramming_speed.begin(), m_filpar[old_tool].ramming_speed.end(), 0.f),
m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator,
layer_height);
// Orca: Reserve ramming depth only when toolchange_Unload() will actually ram,
// otherwise the unprinted reservation leaves blank bands between the purge boxes.
const bool do_ramming = tool_ramming_enabled(old_tool);
// Orca: with the gap wall on a multi-tool printer the ram band is quantized up to
// the whole reserved rows and the wipe restarts at the left-edge boundary on a
// fresh row below it (BBL parity: the old-tool purge is whole rows and the wipe
// always starts at the box corner, where the entry scrub runs).
const bool boundary_wipe_start = boundary_wipe_start_enabled(old_tool);
float ramming_depth = do_ramming ? ((int(length_to_extrude / width) + 1) * (m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator * m_filpar[old_tool].ramming_step_multiplicator) * m_extra_spacing_ramming) : 0;
// first_wipe_line rides for free on the last (partially used) ramming row, which
// is already covered by ramming_depth. Without ramming that row does not exist
// (and with a boundary wipe start the ram band is quantized to whole rows), so
// the whole wipe volume needs reserved wiping depth.
float first_wipe_line = (do_ramming && !boundary_wipe_start) ? - (width*((length_to_extrude / width)-int(length_to_extrude / width)) - width) : 0.f;
float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height);
// ORCA: Keep wipe-depth planning consistent with toolchange_Wipe().
// ORCA: On the first layer, toolchange_Wipe() advances purge rows using
@@ -2245,12 +2103,11 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
// ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width;
// ORCA: Use the same spacing here so reserved depth matches consumed depth
// ORCA: and first-layer purge segments do not leave visible gaps.
const bool first_layer_plan = (m_plan.size() - 1) == m_first_layer_idx;
const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe;
float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, m_perimeter_width, m_extra_flow, planning_spacing, width);
m_plan.back().tool_changes.push_back(WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume));
float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height, m_perimeter_width, m_extra_flow, planning_spacing, width);
return WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume);
}
@@ -2288,49 +2145,64 @@ void WipeTower2::save_on_last_wipe()
continue;
// Which toolchange will finish_layer extrusions be subtracted from?
int idx = first_toolchange_to_nonsoluble(m_layer_info->tool_changes);
int idx = first_toolchange_to_nonsoluble_nonsupport(m_layer_info->tool_changes);
if (idx == -1) {
// In this case, finish_layer will be called at the very beginning.
finish_layer().total_extrusion_length_in_plane();
}
const float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into
auto recompute_toolchange = [this, width](WipeTowerInfo::ToolChange& toolchange, float volume_to_save) {
float volume_left_to_wipe = std::max(m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower, toolchange.wipe_volume_total - volume_to_save);
float volume_we_need_depth_for = std::max(0.f, volume_left_to_wipe - length_to_volume(toolchange.first_wipe_line, m_perimeter_width*m_extra_flow, m_layer_info->height));
// ORCA: Keep wipe-depth planning consistent with toolchange_Wipe().
// ORCA: On the first layer, toolchange_Wipe() advances purge rows using
// ORCA: m_extra_flow * m_perimeter_width, while later layers use
// ORCA: m_extra_spacing_wipe * m_perimeter_width.
// ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width;
// ORCA: Use the same spacing here so reserved depth matches consumed depth
// ORCA: and first-layer purge segments do not leave visible gaps.
const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx;
const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe;
float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width);
toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe;
toolchange.wipe_volume = volume_left_to_wipe;
};
for (int i=0; i<int(m_layer_info->tool_changes.size()); ++i) {
auto& toolchange = m_layer_info->tool_changes[i];
tool_change(toolchange.new_tool);
if (i == idx) {
float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into
float volume_to_save = length_to_volume(finish_layer().total_extrusion_length_in_plane(), m_perimeter_width, m_layer_info->height);
float volume_left_to_wipe = std::max(m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower, toolchange.wipe_volume_total - volume_to_save);
float volume_we_need_depth_for = std::max(0.f, volume_left_to_wipe - length_to_volume(toolchange.first_wipe_line, m_perimeter_width*m_extra_flow, m_layer_info->height));
// ORCA: Keep wipe-depth planning consistent with toolchange_Wipe().
// ORCA: On the first layer, toolchange_Wipe() advances purge rows using
// ORCA: m_extra_flow * m_perimeter_width, while later layers use
// ORCA: m_extra_spacing_wipe * m_perimeter_width.
// ORCA: float dy = (is_first_layer() ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width;
// ORCA: Use the same spacing here so reserved depth matches consumed depth
// ORCA: and first-layer purge segments do not leave visible gaps.
const bool first_layer_plan = size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx;
const float planning_spacing = first_layer_plan ? m_extra_flow : m_extra_spacing_wipe;
float depth_to_wipe = get_wipe_depth(volume_we_need_depth_for, m_layer_info->height, m_perimeter_width, m_extra_flow, planning_spacing, width);
toolchange.required_depth = toolchange.ramming_depth + depth_to_wipe;
toolchange.wipe_volume = volume_left_to_wipe;
recompute_toolchange(toolchange, length_to_volume(finish_layer().total_extrusion_length_in_plane(), m_perimeter_width, m_layer_info->height));
} else if (toolchange.wipe_volume < m_filpar[toolchange.new_tool].filament_minimal_purge_on_wipe_tower) {
// Keep filament_minimal_purge_on_wipe_tower enforced for toolchanges that get
// no finish-layer saving, e.g. a support/soluble filament skipped as the
// finish filament above. Recomputing only when the clamp binds leaves all
// other toolchanges with their planned values bit-for-bit.
recompute_toolchange(toolchange, 0.f);
}
}
}
}
// Return index of first toolchange that switches to non-soluble extruder
// ot -1 if there is no such toolchange.
int WipeTower2::first_toolchange_to_nonsoluble(
// Return the index of the toolchange whose new filament should print the layer's
// finish extrusions (sparse infill + wall + brim), or -1 to print them with the
// layer's incoming filament before any toolchange happens.
// Like WipeTower::first_toolchange_to_nonsoluble_nonsupport(): support and soluble
// filaments bond poorly to the material printed on top of them, so they must not
// print the tower's shell when another filament is available on the layer.
int WipeTower2::first_toolchange_to_nonsoluble_nonsupport(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const
{
if (tool_changes.empty())
return -1;
// If a specific wipe tower filament is forced, use it to decide where to finish the layer.
if (m_wipe_tower_filament > 0) {
for (size_t idx = 0; idx < tool_changes.size(); ++idx) {
@@ -2339,8 +2211,19 @@ int WipeTower2::first_toolchange_to_nonsoluble(
}
return -1;
}
// Orca: allow calculation of the required depth and wipe volume for soluble toolchanges as well.
return tool_changes.empty() ? -1 : 0;
auto is_wall_filament = [this](size_t tool) {
return !m_filpar[tool].is_soluble && !m_filpar[tool].is_support;
};
for (size_t idx = 0; idx < tool_changes.size(); ++idx)
if (is_wall_filament(tool_changes[idx].new_tool))
return idx;
if (is_wall_filament(tool_changes.front().old_tool))
return -1;
// Only support/soluble filaments on this layer: keep the first toolchange so the
// finish-layer saving and the minimal-purge clamp still apply to it (Orca depth
// and wipe volume accounting, see save_on_last_wipe()).
return 0;
}
static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first,
@@ -2363,6 +2246,24 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first,
}
// Precompute, for every plan layer, the wall openings ("skip points") at each toolchange's
// entry position, like WipeTower::get_all_wall_skip_points(). toolchange_entry_pos()
// reproduces from the finalized plan where tool_change() will start, so each gap coincides
// with the entry travel's target (tcr.start_pos, pre-rotation frame). BBL parity: the gap
// sits at the CP_TOOLCHANGE_WIPE start row, never at the ram band.
void WipeTower2::compute_wall_skip_points()
{
m_wall_skip_points.assign(m_plan.size(), std::vector<Vec2f>());
for (size_t layer_id = 0; layer_id < m_plan.size(); ++layer_id) {
float depth_traversed = 0.f;
for (const auto& toolchange : m_plan[layer_id].tool_changes) {
m_wall_skip_points[layer_id].emplace_back(
toolchange_entry_pos(depth_traversed, toolchange.ramming_depth, layer_id == m_first_layer_idx));
depth_traversed += toolchange.required_depth;
}
}
}
// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower
// Resulting ToolChangeResults are appended into vector "result"
void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result)
@@ -2378,12 +2279,41 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
}
#endif
m_rib_length = std::max({m_rib_length, sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width)});
if (m_wall_type == (int)wtwRib) {
// Rib wall: force a square tower like WipeTower::plan_tower_new(), ignoring the
// configured prime_tower_width (the GUI greys it out in rib mode). The planned depths
// already include the extra-spacing factors, so sqrt(depth * width) preserves the
// purge area. Replan every toolchange for the new width, then re-derive the depths.
float max_depth = 0.f;
for (const auto& current_plan : m_plan)
max_depth = std::max(max_depth, current_plan.depth);
if (max_depth > EPSILON) {
m_wipe_tower_width = align_ceil(std::sqrt(max_depth * m_wipe_tower_width), m_perimeter_width);
for (size_t idx = 0; idx < m_plan.size(); ++idx)
for (auto& toolchange : m_plan[idx].tool_changes)
toolchange = set_toolchange(toolchange.old_tool, toolchange.new_tool,
m_plan[idx].height, toolchange.wipe_volume,
idx == m_first_layer_idx);
plan_tower();
}
// Like WipeTower::plan_tower_new(): extend the ribs instead of the tower when the
// tower is smaller than the height-based stability minimum.
const float min_depth = WipeTower::get_limit_depth_by_height(m_wipe_tower_height);
if (m_wipe_tower_depth + EPSILON < min_depth)
m_rib_length = std::max(m_rib_length, min_depth * (float)std::sqrt(2.f));
}
const float diagonal = std::sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width);
m_rib_length = std::max(m_rib_length, diagonal);
m_rib_length += m_extra_rib_length;
m_rib_length = std::max(0.f, m_rib_length);
m_rib_length = std::max(diagonal, m_rib_length); // a negative extra length must not shrink the ribs below the diagonal
m_rib_width = std::min(m_rib_width, std::min(m_wipe_tower_depth, m_wipe_tower_width) /
2.f); // Ensure that the rib wall of the wipetower are attached to the infill.
if (m_use_gap_wall)
compute_wall_skip_points();
m_layer_info = m_plan.begin();
m_current_height = 0.f;
@@ -2410,7 +2340,7 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
if (m_layer_info->depth < m_wipe_tower_depth - m_perimeter_width)
m_y_shift = (m_wipe_tower_depth-m_layer_info->depth-m_perimeter_width)/2.f;
int idx = first_toolchange_to_nonsoluble(layer.tool_changes);
int idx = first_toolchange_to_nonsoluble_nonsupport(layer.tool_changes);
WipeTower::ToolChangeResult finish_layer_tcr;
if (idx == -1) {
@@ -2503,8 +2433,7 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2&
double feedrate,
bool first_layer,
bool rib_wall,
bool extrude_perimeter,
bool skip_points)
bool extrude_perimeter)
{
float retract_length = m_filpar[m_current_tool].retract_length;
@@ -2524,18 +2453,28 @@ Polygon WipeTower2::generate_support_rib_wall(WipeTowerWriter2&
if (!extrude_perimeter)
return wall_polygon;
if (skip_points) {
result_wall = contrust_gap_for_skip_points(wall_polygon, std::vector<Vec2f>(), m_wipe_tower_width, 2.5 * m_perimeter_width,
if (m_use_gap_wall) {
// Cut the wall open at each toolchange's entry (see compute_wall_skip_points()).
// The vector is empty during the save_on_last_wipe planning passes, which therefore
// measure the un-gapped wall — same approximation as the BBL tower.
static const std::vector<Vec2f> no_skip_points;
const size_t layer_id = size_t(m_layer_info - m_plan.begin());
const std::vector<Vec2f>& layer_skip_points =
layer_id < m_wall_skip_points.size() ? m_wall_skip_points[layer_id] : no_skip_points;
result_wall = construct_gap_for_skip_points(wall_polygon, layer_skip_points, m_wipe_tower_width, 2.5 * m_perimeter_width,
insert_skip_polygon);
} else {
result_wall.push_back(to_polyline(wall_polygon));
insert_skip_polygon = wall_polygon;
}
writer.generate_path(result_wall, feedrate, retract_length, retract_speed, m_used_fillet);
//if (m_cur_layer_id == 0) {
// BoundingBox bbox = get_extents(result_wall);
// m_rib_offset = Vec2f(-unscaled<float>(bbox.min.x()), -unscaled<float>(bbox.min.y()));
//}
// Tower-local shift that puts the rib wall's protruding first-layer min corner at the
// configured tower position, like WipeTower::generate_support_wall_new(). Measured on
// the un-gapped outline so a wall gap cannot shift the tower.
if (rib_wall && is_first_layer()) {
BoundingBox bbox = get_extents(insert_skip_polygon);
m_rib_offset = Vec2f(-unscaled<float>(bbox.min.x()), -unscaled<float>(bbox.min.y()));
}
return insert_skip_polygon;
}
+59 -10
View File
@@ -34,6 +34,10 @@ public:
bool is_finish,
bool is_contact = false) const;
// Whether this print cuts wall openings ("skip points") at the toolchange entries.
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
static bool use_gap_wall(const PrintConfig& config);
// x -- x coordinates of wipe tower in mm ( left bottom corner )
// y -- y coordinates of wipe tower in mm ( left bottom corner )
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
@@ -69,9 +73,9 @@ public:
const float brim = m_wipe_tower_brim_width_real;
return BoundingBoxf(Vec2d(-brim, -brim), Vec2d(double(m_wipe_tower_width) + brim, double(m_wipe_tower_depth) + brim));
}
// WT2 doesn't currently compute a rib-origin compensation like WipeTower (m_rib_offset),
// so expose a zero offset for consistency purposes (to maintain API parity).
Vec2f get_rib_offset() const { return Vec2f::Zero(); }
// Tower-local shift that puts the rib wall's first-layer min corner at the configured
// tower position, like WipeTower::get_rib_offset(). Zero unless the rib wall is used.
Vec2f get_rib_offset() const { return m_rib_offset; }
float get_rib_width() const { return m_rib_width; }
float get_rib_length() const { return m_rib_length; }
@@ -149,6 +153,7 @@ public:
struct FilamentParameters {
std::string material = "PLA";
bool is_soluble = false;
bool is_support = false;
int temperature = 0;
int first_layer_temperature = 0;
int interface_print_temperature = 0;
@@ -220,7 +225,6 @@ private:
float m_perimeter_speed = 0.f;
float m_first_layer_speed = 0.f;
size_t m_first_layer_idx = size_t(-1);
bool m_flat_ironing = false;
bool m_enable_tower_interface_features = false;
bool m_enable_tower_interface_cooldown_during_tower = false;
bool m_prev_layer_had_interface = false;
@@ -231,6 +235,12 @@ private:
float m_rib_width = 10;
float m_extra_rib_length = 0;
float m_rib_length = 0;
Vec2f m_rib_offset = Vec2f::Zero();
bool m_use_gap_wall = false;
// Per plan layer, each toolchange's entry position (tower-local, un-shifted frame):
// where the wall is cut open so the entry travel does not cross the printed wall.
// Filled by compute_wall_skip_points() once the plan is final.
std::vector<std::vector<Vec2f>> m_wall_skip_points;
bool m_enable_arc_fitting = false;
@@ -278,6 +288,37 @@ private:
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
// Purge row lattice of toolchange_Wipe(): row pitch and extrusion width.
float wipe_row_spacing(bool first_layer) const { return (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; }
float wipe_line_width() const { return m_perimeter_width * m_extra_flow; }
// Whether toolchange_Unload() rams this (old) tool out.
bool tool_ramming_enabled(size_t tool) const { return (m_semm && m_enable_filament_ramming) || m_filpar[tool].multitool_ramming; }
// Whether the wipe restarts at the box boundary on a fresh row below the quantized
// ram band after ramming this (old) tool out (multi-tool gap wall; SEMM keeps the
// stock continue-from-ram-end behavior).
bool boundary_wipe_start_enabled(size_t tool) const { return tool_ramming_enabled(tool) && !m_semm && m_use_gap_wall; }
// With a boundary wipe start the wipe begins on a fresh row below the quantized ram
// band. Y offset from the box start to that first wipe row.
float wipe_start_offset_after_ram(float ramming_depth, bool first_layer) const
{
return ramming_depth + wipe_row_spacing(first_layer) - (m_perimeter_width + wipe_line_width()) / 2.f;
}
// Tower-local entry position of a toolchange whose box starts depth_traversed into
// the layer: the box corner, moved down to the first wipe row when the plan gives
// it a boundary wipe start (ramming_depth > 0 iff the unload rams). tool_change()
// enters here and compute_wall_skip_points() cuts the wall gap here, so the routed
// entry, the gap and the wipe scrub all share one opening.
Vec2f toolchange_entry_pos(float depth_traversed, float ramming_depth, bool first_layer) const
{
Vec2f pos(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed);
if (!m_semm && m_use_gap_wall && ramming_depth > 0.f)
pos.y() += wipe_start_offset_after_ram(ramming_depth, first_layer);
return pos;
}
// Calculates extrusion flow needed to produce required line width for given layer height
float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow
{
@@ -328,9 +369,10 @@ private:
std::vector<float> m_used_filament_length;
std::vector<std::pair<float, std::vector<float>>> m_used_filament_length_until_layer;
// Return index of first toolchange that switches to non-soluble extruder
// ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble(
// Return the index of the toolchange whose new filament should print the layer's
// finish extrusions (sparse infill + wall + brim), or -1 to print them with the
// layer's incoming filament before any toolchange happens.
int first_toolchange_to_nonsoluble_nonsupport(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
void toolchange_Unload(
@@ -353,7 +395,9 @@ private:
WipeTowerWriter2 &writer,
const WipeTower::box_coordinates &cleaning_box,
float wipe_volume,
bool interface_layer);
bool interface_layer,
bool priming = false,
bool fill_box = false);
Polygon generate_support_rib_wall(WipeTowerWriter2& writer,
@@ -361,8 +405,7 @@ private:
double feedrate,
bool first_layer,
bool rib_wall,
bool extrude_perimeter,
bool skip_points);
bool extrude_perimeter);
Polygon generate_support_cone_wall(
WipeTowerWriter2& writer,
@@ -372,6 +415,12 @@ private:
float spacing);
Polygon generate_rib_polygon(const WipeTower::box_coordinates& wt_box);
void compute_wall_skip_points();
// Computes the depth reserved for a toolchange (shared by plan_toolchange() and the
// rib-wall square-tower replanning in generate()).
WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan);
};
+2 -2
View File
@@ -129,13 +129,13 @@ public:
std::vector<PathFittingData> fitting_result;
//BBS: simplify points by arc fitting
void simplify_by_fitting_arc(double tolerance);
//BBS:
void reset_to_linear_move();
//BBS:
Polylines equally_spaced_lines(double distance) const;
private:
void append_fitting_result_after_append_points();
void append_fitting_result_after_append_polyline(const Polyline& src);
void reset_to_linear_move();
bool split_fitting_result_before_index(const size_t index, Point &new_endpoint, std::vector<PathFittingData>& data) const;
bool split_fitting_result_after_index(const size_t index, Point &new_startpoint, std::vector<PathFittingData>& data) const;
};
+48 -48
View File
@@ -3453,7 +3453,11 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
}
else if ((extruder_volume_type_count > extruder_count) && (m_config.filament_volume_map.values.size() > index))
nozzle_volume_type = (NozzleVolumeType)(m_config.filament_volume_map.values[index]);
m_config.filament_map_2.values[index] = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
// Orca: when the process variant columns cannot be matched (degenerate
// print_extruder_id), key the override by plain extruder index like the seeding
// above instead of poisoning the map with -1.
int slot_index = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : f_maps[index] - 1;
}
m_full_print_config = m_ori_full_print_config;
@@ -4061,10 +4065,33 @@ void Print::_make_wipe_tower()
// in BBL machine, wipe tower is only use to prime extruder. So just use a global wipe volume.
WipeTower wipe_tower(m_config, m_plate_index, m_origin, m_wipe_tower_data.tool_ordering.first_extruder(),
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
// Orca: the tower's first-layer flow follows the user's first-layer flow ratio (BBS reads
// its initial_layer_flow_ratio here — STUDIO-14254; first_layer_flow_ratio is Orca's analog,
// default 1.0 in both). Honor the set_other_flow_ratios gate that governs the option
// everywhere else.
wipe_tower.set_first_layer_flow_ratio(m_default_object_config.set_other_flow_ratios
? float(m_default_region_config.first_layer_flow_ratio)
: 1.f);
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
wipe_tower.set_filament_map(this->get_filament_maps());
// Vortek H2C: pass nozzle-level map for carousel rotation detection in tool_change_new()
wipe_tower.set_filament_nozzle_map(this->get_filament_nozzle_maps());
// Per-layer filament->nozzle grouping. sort_and_build_data() above publishes it on the Print
// for by-layer prints; by-object prints publish only later (psSkirtBrim), so fall back to the
// ToolOrdering's own copy there. set_extruder() below dereferences it, so it must be set first.
auto print_group_result = get_layered_nozzle_group_result();
const MultiNozzleUtils::LayeredNozzleGroupResult &nozzle_group_result =
print_group_result ? *print_group_result : m_wipe_tower_data.tool_ordering.get_layered_nozzle_group_result();
wipe_tower.set_nozzle_group_result(nozzle_group_result);
{
// Orca: acceleration options are object-scope (PrintConfig members in BBS), so resolve
// the per-variant columns here; initial_layer_travel_acceleration is FloatOrPercent
// over travel_acceleration and needs the full config to resolve.
std::vector<double> first_layer_travel_accels;
for (size_t i = 0; i < m_config.initial_layer_travel_acceleration.values.size(); ++i)
first_layer_travel_accels.emplace_back(m_full_print_config.get_abs_value_at("initial_layer_travel_acceleration", i));
wipe_tower.set_accelerations(m_default_object_config.default_acceleration.values,
m_default_object_config.initial_layer_acceleration.values,
m_default_object_config.travel_acceleration.values,
first_layer_travel_accels);
}
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
@@ -4100,27 +4127,19 @@ void Print::_make_wipe_tower()
multi_extruder_flush.emplace_back(wipe_volumes);
}
// Use NozzleStatusRecorder for per-carousel-slot tracking (BBS pattern).
// The original Orca code tracked per-extruder (2 slots), which collapsed all
// carousel filaments into one slot and caused massive redundant AMS flushing.
auto group_result = get_layered_nozzle_group_result();
// Per-carousel-slot purge tracking via NozzleStatusRecorder (BBS pattern); the layered
// group result set on the tower above resolves each filament to its nozzle slot per layer.
MultiNozzleUtils::NozzleStatusRecorder nozzle_recorder;
// Fallback (group_result == null) per-physical-nozzle tracking, matching the original
// pre-port behavior: remembers the last filament loaded in each physical nozzle slot.
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, (unsigned int) -1);
std::vector<int>filament_maps = get_filament_maps();
int layer_idx = -1;
unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder();
// Initialize NozzleStatusRecorder with the first filament's carousel slot
if (group_result) {
auto nozzle = group_result->get_nozzle_for_filament(current_filament_id, layer_idx);
{
auto nozzle = nozzle_group_result.get_nozzle_for_filament(current_filament_id, layer_idx);
if (nozzle)
nozzle_recorder.set_nozzle_status(nozzle->group_id, current_filament_id, nozzle->extruder_id);
} else {
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1;
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id;
}
for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
@@ -4139,8 +4158,8 @@ void Print::_make_wipe_tower()
float volume_to_purge = 0;
// Per-carousel-slot purge tracking via NozzleStatusRecorder
if (group_result) {
auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_idx);
{
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_id, layer_idx);
if (nozzle_info) {
int extruder_id = nozzle_info->extruder_id;
int nozzle_id = nozzle_info->group_id;
@@ -4159,22 +4178,6 @@ void Print::_make_wipe_tower()
}
nozzle_recorder.set_nozzle_status(nozzle_id, filament_id, extruder_id);
}
} else {
// Fallback: original Orca per-physical-nozzle path (non-carousel printers).
// Flush source is the last filament that occupied THIS nozzle, guarded so the
// first use of a nozzle incurs no flush.
int nozzle_id = filament_maps[filament_id] - 1;
unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id];
if (pre_filament_id != (unsigned int) -1 && pre_filament_id != filament_id) {
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
? m_config.flush_multiplier_fast.get_at(nozzle_id)
: m_config.flush_multiplier.get_at(nozzle_id);
volume_to_purge *= flush_multiplier;
volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
*this, current_filament_id, filament_id, volume_to_purge);
}
nozzle_cur_filament_ids[nozzle_id] = filament_id;
}
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
@@ -4182,29 +4185,21 @@ void Print::_make_wipe_tower()
float grab_purge_volume = m_config.grab_length.get_at(grab_extruder_id) * 2.4; //(diameter/2)^2*PI=2.4
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
// Select prime volume per-filament: nozzle change (carousel rotation) uses
// filament_prime_volume_nc, filament change (same nozzle slot) uses filament_prime_volume.
// Prime volume per-filament: the tower now picks extruder-change vs nozzle-change
// (carousel) internally per plan layer, so pass both candidates (BBS pattern).
float wipe_volume_ec = filament_id < m_config.filament_prime_volume.values.size()
? m_config.filament_prime_volume.values[filament_id]
: (float) m_config.prime_volume;
float wipe_volume_nc = filament_id < m_config.filament_prime_volume_nc.values.size()
? m_config.filament_prime_volume_nc.values[filament_id]
: (float) m_config.prime_volume;
float prime_volume = wipe_volume_ec;
if (group_result) {
bool is_nozzle_change = group_result->are_filaments_same_extruder(current_filament_id, filament_id, layer_idx) &&
!group_result->are_filaments_same_nozzle(current_filament_id, filament_id, layer_idx);
if (is_nozzle_change) {
prime_volume = wipe_volume_nc;
}
}
if (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) {
prime_volume = 15.f;
wipe_volume_ec = 15.f;
wipe_volume_nc = 15.f;
}
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
prime_volume, volume_to_purge);
wipe_volume_ec, wipe_volume_nc, volume_to_purge);
current_filament_id = filament_id;
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
@@ -4382,7 +4377,12 @@ void Print::_make_wipe_tower()
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
config().wipe_tower_fillet_wall.value);
const Vec3d origin = Vec3d::Zero();
m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position(), wipe_tower.width(), wipe_tower.get_wipe_tower_height(),
// FakeWipeTower::pos is a bed-frame translation applied after rotation
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
// tower-local rib offset must be rotated into the bed frame first.
m_fake_wipe_tower.rib_offset = Eigen::Rotation2Df(Geometry::deg2rad((float)config().wipe_tower_rotation_angle.value)) *
wipe_tower.get_rib_offset();
m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position() + m_fake_wipe_tower.rib_offset, wipe_tower.width(), wipe_tower.get_wipe_tower_height(),
config().initial_layer_print_height, m_wipe_tower_data.depth,
m_wipe_tower_data.z_and_depth_pairs, m_wipe_tower_data.brim_width,
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
+15 -1
View File
@@ -1375,7 +1375,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps
&& opt_filament_volume_maps->values.size() == filament_maps.size())
nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]);
m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
// Orca: when the process variant columns cannot be matched (degenerate
// print_extruder_id), key the override by plain extruder index like the seeding
// above instead of poisoning the map with -1.
int slot_index = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : filament_maps[index] - 1;
}
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
@@ -1431,6 +1435,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
num_extruders_changed = true;
}
}
else if (! print_diff.empty()) {
// Orca: m_config can diverge from an unchanged full config (e.g. the in-slice retract
// override recompute writing different values than the apply-time computation). The
// invalidation above already fired for print_diff, so repair m_config here as well;
// otherwise the divergence is never corrected and every subsequent apply of the same
// config invalidates the result again, forever.
m_placeholder_parser.apply_config(filament_overrides);
m_config.apply_only(new_full_config, print_diff, true);
m_config.apply(filament_overrides);
}
ModelObjectStatusDB model_object_status_db;
+40
View File
@@ -10663,6 +10663,44 @@ int DynamicPrintConfig::get_extruder_nozzle_volume_count(int extruder_count, std
return count;
}
// Orca: BBL system profiles ship full-width print_extruder_id/print_extruder_variant columns, but
// custom multi-extruder printers only ever get the machine-scope columns synthesized for them (see
// extend_extruder_variant); the process scope keeps the length-1 defaults, both in presets and in
// 3mf project configs. Expanding with that degenerate map makes every per-extruder lookup fail, and
// because both keys are themselves in print_options_with_variant, the expansion then latches a
// full-width-but-wrong [1,1,...] map that also defeats the generated_extruder_id fallback in
// get_index_for_extruder. Synthesize the process columns from the printer's extruder_variant_list
// (same token walk as extend_extruder_variant) before expanding.
static void ensure_process_variant_columns(DynamicPrintConfig &config, const DynamicPrintConfig &printer_config)
{
auto id_opt = dynamic_cast<ConfigOptionInts *>(config.option("print_extruder_id"));
auto variant_opt = dynamic_cast<ConfigOptionStrings *>(config.option("print_extruder_variant"));
auto list_opt = dynamic_cast<const ConfigOptionStrings *>(printer_config.option("extruder_variant_list"));
if (!id_opt || !variant_opt || !list_opt)
return;
if (id_opt->values.size() != 1 || variant_opt->values.size() != 1)
return;
std::vector<int> ids;
std::vector<std::string> variants;
for (int i = 0; i < int(list_opt->values.size()); ++i) {
std::vector<std::string> tokens;
boost::split(tokens, list_opt->get_at(i), boost::is_any_of(","), boost::token_compress_on);
for (std::string &token : tokens) {
boost::trim(token);
if (token.empty())
continue;
ids.push_back(i + 1);
variants.push_back(token);
}
}
// A single column is the legitimate single-extruder layout, not a degenerate one.
if (ids.size() <= 1)
return;
id_opt->values = std::move(ids);
variant_opt->values = std::move(variants);
}
std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector<std::vector<NozzleVolumeType>>& nv_types,
std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride, unsigned int extruder_id, NozzleVolumeType filament_nvt)
{
@@ -10704,6 +10742,8 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
variant_count = 1;
}
else {
if (id_name == "print_extruder_id")
ensure_process_variant_columns(*this, printer_config);
// Orca: emit the slots first, then size variant_count from what was actually
// emitted. extruder_nozzle_volume_count only equals the emitted total when every
// extruder carries per-type stats; an extruder with an empty stats entry combined
+1
View File
@@ -3389,6 +3389,7 @@ arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const Dynamic
bool enable_wrapping = (wrapping_opt != nullptr) && wrapping_opt->value;
wt_size = estimate_wipe_tower_size(config, w, v, extruder_count, plate_extruder_size, use_global_objects, enable_wrapping);
int plate_width=m_width, plate_depth=m_depth;
w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower
float depth = wt_size(1);
float margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f;
const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width");
+2
View File
@@ -11,6 +11,7 @@ namespace Slic3r
// IMPORTANT: ordinal order is the Plugins dialog Source sort priority.
Mine,
Subscribed,
Orphaned,
Local
};
@@ -20,6 +21,7 @@ namespace Slic3r
{
case PluginSource::Mine: return "mine";
case PluginSource::Subscribed: return "subscribed";
case PluginSource::Orphaned: return "orphaned";
case PluginSource::Local: return "local";
}
+44 -9
View File
@@ -103,6 +103,7 @@ struct PluginDialogItem
bool loading = false;
bool is_cloud_plugin = false;
bool orphaned = false;
bool has_local_package = false;
bool unauthorized = false;
bool has_script_capability = false;
@@ -246,6 +247,7 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
payload_item["sharing_token"] = dialog_item.sharing_token;
payload_item["thumbnail_url"] = dialog_item.thumbnail_url;
payload_item["installed"] = dialog_item.has_local_package;
payload_item["orphaned"] = dialog_item.orphaned;
payload_item["installed_version"] = dialog_item.installed_version;
payload_item["latest_version"] = dialog_item.latest_version;
return payload_item;
@@ -268,7 +270,8 @@ PluginSource derive_plugin_source(const PluginDescriptor& descriptor)
const bool is_cloud = descriptor.is_cloud_plugin();
const bool is_mine = is_cloud && has_cloud_meta && descriptor.cloud->is_mine;
// Source is ownership/locality only; issue states never replace this badge.
if (is_cloud && has_cloud_meta && descriptor.cloud->orphaned)
return PluginSource::Orphaned;
if (is_mine)
return PluginSource::Mine;
if (is_cloud)
@@ -281,11 +284,12 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
PluginAvailableActions available_actions;
const bool is_loading = item.status == PluginStatus::Loading;
const bool is_cloud = item.is_cloud_plugin;
const bool is_orphaned = item.orphaned;
const bool is_mine = item.source == PluginSource::Mine;
const bool has_local = item.has_local_package;
const bool authorized_for_install = !item.unauthorized;
available_actions.toggle_installs_cloud_plugin = is_cloud && !has_local && authorized_for_install;
available_actions.toggle_installs_cloud_plugin = is_cloud && !is_orphaned && !has_local && authorized_for_install;
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
@@ -294,7 +298,7 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
// Owned cloud plugins fall through to the local delete: it removes the installed package only.
// Deleting a plugin from the cloud is a plugin hub operation and is never offered here.
if (is_cloud && !is_mine) {
if (is_cloud && !is_orphaned && !is_mine) {
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
} else if (has_local) {
add_action("delete_plugin", "Delete", true, true);
@@ -302,11 +306,13 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
add_action("open_folder", "Show in folder", has_local);
if (is_cloud) {
add_action("reinstall_plugin", "Reinstall");
} else {
add_action("reload_plugin", "Reload");
add_action("clear_cache_reload_plugin", "Delete cache and reload");
if (!is_orphaned) {
if (is_cloud) {
add_action("reinstall_plugin", "Reinstall");
} else {
add_action("reload_plugin", "Reload");
add_action("clear_cache_reload_plugin", "Delete cache and reload");
}
}
return available_actions;
@@ -360,6 +366,7 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
item.error_text = descriptor.normalized_error();
item.has_error = descriptor.has_error();
item.is_cloud_plugin = descriptor.is_cloud_plugin();
item.orphaned = descriptor.cloud.has_value() && descriptor.cloud->orphaned;
item.has_local_package = descriptor.has_local_package();
item.unauthorized = descriptor.is_unauthorized();
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
@@ -592,7 +599,35 @@ bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescript
void PluginsDialog::refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud)
{
run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() { send_plugins(); }, title, message);
run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() {
prompt_for_missing_plugins();
send_plugins();
}, title, message);
}
void PluginsDialog::prompt_for_missing_plugins()
{
PluginManager& manager = PluginManager::instance();
const std::vector<PluginDescriptor> missing = manager.get_missing_plugin_descriptors();
if (missing.empty())
return;
wxString names;
std::vector<std::string> keys;
keys.reserve(missing.size());
for (const PluginDescriptor& plugin : missing) {
keys.push_back(plugin.plugin_key);
names += "\n- ";
names += plugin_display_name(plugin.plugin_key);
}
const int result = wxMessageBox(
wxString::Format(_L("The following installed plugins were not found on disk:\n%s\n\nRemove them from OrcaSlicer?"), names),
_L("Missing Plugins"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (result == wxYES)
manager.remove_missing_plugins(keys);
}
void PluginsDialog::refresh_plugins()
+1
View File
@@ -68,6 +68,7 @@ private:
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
void refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud);
void prompt_for_missing_plugins();
void refresh_plugins();
void toggle_plugin(const std::string& plugin_key, bool enabled);
void toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name, bool enabled);
+39 -57
View File
@@ -1,12 +1,14 @@
#include "PresetUpdater.hpp"
#include <algorithm>
#include <boost/filesystem/directory.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/nowide/fstream.hpp>
#include <functional>
#include <atomic>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <ostream>
@@ -19,6 +21,7 @@
#include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp>
#include <vector>
#include <wx/app.h>
#include <wx/msgdlg.h>
@@ -204,7 +207,6 @@ struct PresetUpdater::priv
// Per-vendor update checking
std::set<std::string> checked_vendors;
std::mutex vendor_check_mutex;
std::vector<std::thread> vendor_check_threads;
std::atomic<bool> vendor_check_cancel{false};
@@ -221,10 +223,10 @@ struct PresetUpdater::priv
priv();
void set_download_prefs(AppConfig *app_config);
bool get_file(const std::string &url, const fs::path &target_path) const;
//BBS: refine preset update logic
bool get_file(const std::string &url, const fs::path &target_path) const;
//BBS: refine preset update logic
bool extract_file(const fs::path &source_path, const fs::path &dest_path = {});
void prune_tmps() const;
void prune_tmp(const std::string& vendor_id) const;
void sync_version() const;
void parse_version_string(const std::string& body) const;
void sync_resources(std::string http_url, std::map<std::string, Resource> &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
@@ -371,14 +373,14 @@ bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::pa
return true;
}
// Remove leftover paritally downloaded files, if any.
void PresetUpdater::priv::prune_tmps() const
// Remove a leftover partial archive for the vendor about to be synchronized.
void PresetUpdater::priv::prune_tmp(const std::string& vendor_id) const
{
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_path))
if (is_plain_file(dir_entry) && dir_entry.path().extension() == TMP_EXTENSION) {
BOOST_LOG_TRIVIAL(debug) << "[Orca Updater]remove old cached files: " << dir_entry.path().string();
fs::remove(dir_entry.path());
}
boost::system::error_code ec;
const fs::path tmp_path = cache_path / (vendor_id + TMP_EXTENSION);
fs::remove(tmp_path, ec);
if (ec)
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]failed to remove " << tmp_path.string() << ": " << ec.message();
}
//BBS: refine the Preset Updater logic
@@ -1060,10 +1062,9 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
Semver resource_ver = get_version_from_json(file_path);
Semver vendor_ver = get_version_from_json(path_in_vendor.string());
bool version_match = ((resource_ver.maj() == vendor_ver.maj()) && (resource_ver.min() == vendor_ver.min()));
if (!version_match || (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();
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);
}
}
@@ -1305,49 +1306,29 @@ PresetUpdater::~PresetUpdater()
//BBS: change directories by design
//BBS: refine the preset updater logic
void PresetUpdater::sync(std::string http_url, std::string language, std::string plugin_version, PresetBundle *preset_bundle)
void PresetUpdater::sync(std::string http_url, std::string language, std::string plugin_version, PresetBundle * /*preset_bundle*/)
{
//p->set_download_prefs(GUI::wxGetApp().app_config);
if (!p->enabled_version_check && !p->enabled_config_update) { return; }
// Copy the whole vendors data for use in the background thread
// Unfortunatelly as of C++11, it needs to be copied again
// into the closure (but perhaps the compiler can elide this).
VendorMap vendors = preset_bundle ? preset_bundle->vendors : VendorMap{};
// Determine active vendor before entering the thread
std::string active_vendor;
if (preset_bundle) {
const Preset& printer = preset_bundle->printers.get_edited_preset();
if (printer.vendor)
active_vendor = printer.vendor->id;
}
p->thread = std::thread([this, vendors, active_vendor, http_url, language, plugin_version]() {
this->p->prune_tmps();
if (p->cancel)
return;
this->p->sync_version();
if (p->cancel)
return;
// Per-vendor config check for the active vendor at startup
if (!active_vendor.empty() && !vendors.empty()) {
this->p->sync_vendor_config(active_vendor);
if (p->cancel)
return;
{
std::lock_guard<std::mutex> lock(this->p->vendor_check_mutex);
this->p->checked_vendors.insert(active_vendor);
}
}
if (p->cancel)
return;
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
//if (p->cancel)
// return;
//remove the tooltip currently
//this->p->sync_tooltip(http_url, language);
p->thread = std::thread([this, http_url, language, plugin_version]() {
try {
this->p->sync_version();
if (p->cancel)
return;
// Vendor profile updates are triggered by check_vendor_update()
// after the startup printer preset has been restored.
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
//if (p->cancel)
// return;
//remove the tooltip currently
//this->p->sync_tooltip(http_url, language);
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater] background sync failed: " << e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater] background sync failed with an unknown exception";
}
});
}
@@ -1356,16 +1337,17 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id)
if (!p->enabled_config_update) return;
if (vendor_id.empty()) return;
std::lock_guard<std::mutex> lock(p->vendor_check_mutex);
if (!p->checked_vendors.insert(vendor_id).second)
return;
p->vendor_check_threads.emplace_back([this, vendor_id]() {
try {
this->p->prune_tmp(vendor_id);
this->p->sync_vendor_config(vendor_id);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater] vendor update failed for " << vendor_id << ": " << e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater] vendor update failed for " << vendor_id << " with an unknown exception";
}
});
}
+3
View File
@@ -20,6 +20,7 @@ struct CloudPluginState
bool update_available = false; // Cloud version > the local package version.
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
bool orphaned = false; // Cloud identity remains locally, but the plugin is no longer subscribed/available.
};
enum class PluginUpdateStatus
@@ -106,6 +107,8 @@ struct PluginDescriptor
{
if (!cloud.has_value())
return PluginUpdateStatus::Normal;
if (cloud->orphaned)
return PluginUpdateStatus::Normal;
if (cloud->unauthorized)
return PluginUpdateStatus::Unauthorized;
if (cloud->update_available)
+1 -2
View File
@@ -120,8 +120,7 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std:
}
if (removed_count == 0) {
error = "Plugin folder was not found: " + resolved_root.string();
return false;
return true;
}
BOOST_LOG_TRIVIAL(info) << "Deleted plugin: " << plugin_id << " from " << resolved_root.string();
+65 -18
View File
@@ -310,6 +310,7 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
}
seen.push_back(descriptor.plugin_key);
m_missing_plugin_keys.erase(descriptor.plugin_key);
Plugin* existing = find_plugin_locked(descriptor.plugin_key);
if (existing == nullptr) {
@@ -330,12 +331,47 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
return;
}
// Unloading may call Python and lifecycle subscribers may re-enter the manager, so never do it
// while holding m_mutex. unload_and_erase_if() retries until no matching entry is loaded at the
// moment of erase, in case another caller starts a load between the initial snapshot and the
// teardown.
unload_and_erase_if(
[&seen](const Plugin& plugin) { return std::find(seen.begin(), seen.end(), plugin.descriptor.plugin_key) == seen.end(); });
// A package can be temporarily absent while an external side-loader replaces it. Keep the
// descriptor and its persisted enable state until the user explicitly removes the missing
// entry, or a later scan rediscovers it. In particular, do not unload here: the unload callback
// would turn a transient filesystem gap into enabled=false in the sidecar.
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
if (plugin.descriptor.has_local_package() &&
std::find(seen.begin(), seen.end(), plugin.descriptor.plugin_key) == seen.end())
m_missing_plugin_keys.insert(plugin.descriptor.plugin_key);
}
}
}
std::vector<PluginDescriptor> PluginManager::get_missing_plugin_descriptors() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<PluginDescriptor> result;
result.reserve(m_missing_plugin_keys.size());
for (const Plugin& plugin : m_plugins)
if (m_missing_plugin_keys.count(plugin.descriptor.plugin_key) != 0)
result.push_back(plugin.descriptor);
return result;
}
void PluginManager::remove_missing_plugins(const std::vector<std::string>& plugin_keys)
{
const std::unordered_set<std::string> requested(plugin_keys.begin(), plugin_keys.end());
// The predicate is evaluated only while m_mutex is held by unload_and_erase_if(). Checking the
// current missing set here prevents a package that reappeared between the dialog and removal
// from being erased.
unload_and_erase_if([this, &requested](const Plugin& plugin) {
return requested.count(plugin.descriptor.plugin_key) != 0 &&
m_missing_plugin_keys.count(plugin.descriptor.plugin_key) != 0;
});
std::lock_guard<std::mutex> lock(m_mutex);
for (const std::string& plugin_key : requested)
m_missing_plugin_keys.erase(plugin_key);
}
void PluginManager::unload_and_erase_if(const std::function<bool(const Plugin&)>& should_remove,
@@ -1422,7 +1458,8 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
std::vector<PluginDescriptor> cloud_list{};
std::vector<std::string> not_found{}, unauthorized{};
if (!m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized)) {
const bool cloud_fetch_succeeded = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
if (!cloud_fetch_succeeded) {
if (wxTheApp != nullptr) {
GUI::wxGetApp().CallAfter([] {
if (GUI::wxGetApp().is_closing())
@@ -1437,9 +1474,10 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
}
}
update_cloud_metadata(cloud_list);
if (cloud_fetch_succeeded)
update_cloud_metadata(cloud_list);
{
if (cloud_fetch_succeeded) {
std::lock_guard<std::mutex> lock(m_mutex);
// Clear the previous cloud verdicts before re-applying the fresh ones.
@@ -1448,19 +1486,28 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
if (!entry.is_cloud_plugin())
continue;
entry.set_unauthorized(false);
if (entry.cloud.has_value())
entry.cloud->orphaned = false;
if (entry.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
entry.clear_error();
}
for (const std::string& uuid : not_found) {
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin() || entry.cloud_uuid() != uuid)
continue;
if (!entry.has_local_package())
entry.set_error(CLOUD_PLUGIN_NOT_FOUND_ERROR);
break;
}
// A successful subscriptions response may report missing UUIDs explicitly, or it may
// simply omit an unsubscribed plugin from `data`. Both cases leave a locally retained
// cloud package orphaned. Owned plugins are returned by the separate mine endpoint and
// must not be orphaned merely because they are not subscribed.
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin() || entry.cloud->is_mine)
continue;
const bool explicitly_not_found = std::find(not_found.begin(), not_found.end(), entry.cloud_uuid()) != not_found.end();
const bool returned_by_cloud = std::any_of(cloud_list.begin(), cloud_list.end(), [&entry](const PluginDescriptor& cloud_entry) {
return cloud_entry.cloud_uuid() == entry.cloud_uuid();
});
entry.cloud->orphaned = explicitly_not_found || !returned_by_cloud;
if (entry.cloud->orphaned)
entry.cloud->update_available = false;
}
for (const std::string& uuid : unauthorized) {
+6
View File
@@ -132,6 +132,11 @@ public:
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Same, but only for packages that are loadable (i.e. not an invalid package).
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Packages that were present in the previous discovery pass but were not found on disk in the
// latest rescan. They are retained until the user explicitly removes them or a later scan finds
// them again.
std::vector<PluginDescriptor> get_missing_plugin_descriptors() const;
void remove_missing_plugins(const std::vector<std::string>& plugin_keys);
// Packages whose .install_state.json marks them for auto-load.
std::vector<std::string> get_enabled_plugin_keys() const;
// The package owning a loaded capability, for the by-name dispatch path.
@@ -262,6 +267,7 @@ private:
// Every discovered plugin, loaded or not. module == nullptr => not loaded.
std::vector<Plugin> m_plugins;
std::unordered_set<std::string> m_missing_plugin_keys;
std::unordered_set<std::string> m_load_in_progress;
// Keys whose in-flight load has been cancelled. Cancellation does NOT remove the key from