Compare commits

..
Author SHA1 Message Date
fa1k3 f32591ed64 Check the real print height: count the Z shrinkage compensation once and include the raft (#16313)
The build height check in Print::validate() compared object layers that already include the Z shrinkage compensation against printable_height divided by that compensation. That counted the compensation twice, so objects that fit were rejected. The check also left out the raft, so an object whose top plus the raft is above printable_height passed validation.

The check now adds object_print_z_min to the top layer and applies the compensation once. The shrinkage message is shown only when the object fits without the compensation. Belt printers apply the compensation to the raw model height, since their height does not come from the layers.
2026-10-13 19:36:47 -05:00
packerlschupfer db9917e722 Http: drop the redundant boost::filesystem alias in CurlGlobalInit (#16446)
The constructor re-declares `namespace fs = boost::filesystem;`, shadowing the
identical file-scope alias the rest of the file already uses. Removing it
leaves every `fs::` in the file resolving to that one declaration.

No functional change.
2026-10-13 19:06:43 -05:00
packerlschupfer cb752d38a3 Probe config-supplied paths without throwing on an unreadable path (#15669)
The startup probes for download_path and last_backup_path used the throwing
boost::filesystem overloads, so a path that could not be probed at all, such as
one whose parent is not searchable, raised filesystem_error instead of falling
back. init_download_path() runs from the GUI_App constructor inside GUI_Run()'s
try, which left the application showing a message box and exiting with no window
and no log file, the throw preceding the log sink. has_restore_data() runs from
the queued EVT_RESTORE_PROJECT handler, whose try starts after the call.

Five probes in those three functions take the error_code overload instead.
Nothing branches on ec, and readable or missing paths are unaffected because both
overloads return the same value for them. A new test covers has_restore_data()
against a backup directory whose parent has no search permission, and skips on
Windows, as root, and where the mode change is refused or not enforced.
2026-10-13 19:05:36 -05:00
Ian Bassi 5bce937d07 Fix wiki links in the process settings tab (#16451)
- Slicing pipeline plugin linked to others_settings_plugin_picker, a page
  that doesn't exist. It now opens the Slicing Pipeline section of the
  plugin types page.
- Infill shift step linked to the Cross Hatch pattern, which doesn't use
  it. It only applies to Cross Zag and Locked Zag, so it now opens Cross
  Zag.
- Fuzzy skin and Fuzzy skin generator mode were crossed: the generator
  mode opened the section that documents Fuzzy skin's values. Each now
  opens its own section.
- The process Change extrusion role G-code had no wiki link; it now has
  one, like the printer and filament versions.
- The hidden Frequent page linked Wall loops to the top of the walls page
  instead of its section, which made the wiki sync write its metadata
  twice.
2026-10-13 20:41:03 -03:00
TheLegendTubaGuy de3af0b7de Add Moonraker trusted-client auth support (#15550)
Orca sent `X-Api-Key: 88888888` to Moonraker whenever the printer preset's API Key was empty, which Moonraker rejects with HTTP 401 since Arksine/moonraker@93f5d9c. The preset's API Key is now the only thing that sets the header, and an empty key sends none.

Logging out of a printer now marks it revoked rather than only clearing its code, since an empty code is a valid Moonraker binding. Discovery events are delivered through the network agent and dropped if their printer agent was replaced while queued.
2026-10-13 15:13:07 -05:00
Damir GaleevandIan Bassi 5a427f2eb6 Precise Seam: new full-coverage policy for Seam Blocked (#16170)
* Precise Seam: new full-coverage policy for Seam Blocked

A Seam Blocked modifier that covers a whole perimeter now blocks it, like a
perimeter painted red all round, instead of being skipped. It overrides
painting and lower-priority modifiers, like Enforced and Neutral. A new
warning names the modifier: "Seam Blocked modifier "<name>" of "<object>"
covers an entire perimeter". It is shown only when a perimeter stays
entirely Blocked after all weak modifiers. The full-containment warning now
covers only Center, Left and Right.

* Precise Seam: fix seams on perimeters blocked all round

- SeamComparator::is_first_not_much_worse() compares the types first, so two
  Blocked candidates fall through like Neutral ones. On a perimeter blocked
  all round, by a whole Seam Blocked modifier or by red painting, Random put
  the seam on the closing vertex of every layer and Aligned stopped aligning.
- The whole-perimeter Blocked warning flags only the highest whole zone, the
  one in effect. A lower whole zone it overwrites is no longer reported, and
  the candidate scan runs only when that zone is Blocked.
- The warning msgids take the type name from the menu's "Seam Blocked" msgid.
- PreciseSeamWarnings::set() shares the load-before-store idiom, and every
  weak segment records its modifier.
- Tests: a whole Blocked perimeter gets the seams of an unmarked one under
  every seam position; overwritten whole Blocked zones are not reported.

---------

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-10-13 16:38:08 -03:00
32 changed files with 895 additions and 184 deletions
+38 -22
View File
@@ -82,9 +82,7 @@ there.
strong point is placed, no later strong modifier and no weak modifier is
processed for that perimeter.
- **Weak:** every weak modifier applies. They are applied from the lowest
priority to the highest, so the highest one overwrites overlapping zones. A
Blocked modifier that fully contains a perimeter is the exception: it is
skipped there (see [Full containment](#full-containment)).
priority to the highest, so the highest one overwrites overlapping zones.
A strong modifier without a usable segment, even one whose fragments were all
discarded, passes the turn to the next one.
@@ -232,20 +230,23 @@ begin and end positions on the source contour.
### Full containment
A modifier that covers the whole perimeter has no boundaries on it. The policy
follows seam painting, where painting a whole perimeter green is a meaningful
choice and forbidding the seam all round is not:
follows seam painting, where a whole perimeter can be painted in either color:
- **Seam Enforced** types the whole perimeter, like a perimeter painted green all
round, with subdivision applied as described under [Weak modifiers](#weak-modifiers).
- **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter,
clearing painting and lower zones.
- **Seam Blocked** is skipped for the perimeter, with the full-containment
warning. The seam cannot avoid the whole perimeter, so the modifier does not
override anything below it: lower zones and painting stay in effect.
- **Seam Center, Left and Right** are skipped with the same warning: there is no
intersection to place the point on.
- **Seam Blocked** types the whole perimeter Blocked, like a perimeter painted
red all round, overriding painting and lower zones. The seam cannot avoid such
a perimeter: the seam placer compares Blocked candidates with each other like
Neutral ones, so the seam is placed as on an unmarked perimeter. If the
perimeter stays Blocked after all weak zones, it is reported in the user
warning (see [Diagnostics and warnings](#diagnostics-and-warnings)), so the
user can check the modifier.
- **Seam Center, Left and Right** are skipped with the full-containment warning:
there is no intersection to place the point on.
Enforced and Neutral take part in the usual priority order (see
Enforced, Neutral and Blocked take part in the usual priority order (see
[Weak modifiers](#weak-modifiers)).
The perimeter is fully contained when the united intervals cover every source
@@ -311,9 +312,9 @@ into every perimeter that has a strong seam.
`collect_weak_modifier_segments()` extracts the segments of every weak modifier
before the polygon is modified, so all positions refer to the same contour. Each
segment becomes a zone with a type and two boundaries, kept in application
order, lowest priority first. Full containment of an Enforced or Neutral
modifier becomes a whole-perimeter zone at its place in that order: it has no
boundaries and takes part in no insertion or helper step below. The boundaries
order, lowest priority first. Full containment of a weak modifier becomes a
whole-perimeter zone at its place in that order: it has no boundaries and takes
part in no insertion or helper step below. The boundaries
carry their positions on the source contour; these remain as provenance after
insertion and are not indices into the modified polygon.
@@ -384,9 +385,10 @@ G-code export issues it as one non-critical warning with the ID
`SlicingPreciseSeamWarning`. It is a single line, "Precise Seam: <causes>. Seam
placement may differ from expected.", because the export warnings dialog shows
only the first line of each warning. Repeated warning events replace the
notification instead of appending to it. Except for the "had no effect" cause,
the causes name the modifier types involved, as the menu names them, in menu
order and each type once, for example "(Seam Left, Seam Enforced)".
notification instead of appending to it. Except for the "covers an entire
perimeter" and "had no effect" causes, the causes name the modifier types
involved, as the menu names them, in menu order and each type once, for example
"(Seam Left, Seam Enforced)".
The causes are:
- **failed to process some intersections (types):** at least one fragment was
@@ -395,8 +397,18 @@ The causes are:
a Seam Center, Left or Right modifier had more than one segment on a
perimeter (see [Strong modifiers](#strong-modifiers)).
- **a perimeter is fully inside a modifier, the modifier was not applied to it
(types):** a Seam Center, Left, Right or Blocked modifier was skipped for a
(types):** a Seam Center, Left or Right modifier was skipped for a
perimeter (see [Full containment](#full-containment)).
- **Seam Blocked modifier "<name>" of "<object>" covers an entire perimeter:**
a Seam Blocked modifier covered a whole perimeter (see
[Full containment](#full-containment)). Only perimeters whose candidates are
all Blocked after all weak zones contribute to this warning, and only through
the zone in effect there: `apply_weak_modifiers_to_perimeter()` checks the
final types and flags the modifier of the highest whole-perimeter zone when it
is Blocked. A lower whole zone is overwritten by it and is not reported. Only
the first flagged modifier in print and volume order is named, followed by
"(N in total)" when there are several. The type name reuses the menu's
translation.
- **modifier "<name>" of "<object>" had no effect on the seam (it might not reach
the centerline of the printed perimeter):** a modifier was evaluated on at
least one perimeter and never gave a segment, full containment or a discarded
@@ -420,6 +432,8 @@ The log records the following diagnostic markers:
- `[PreciseSeamNoEffect]` for every modifier of the "had no effect" cause, with
the object and modifier names. Unlike the user warning, the log lists all of
them.
- `[PreciseSeamFullyBlocked]` for every modifier of the "covers an entire
perimeter" cause, listed the same way.
Failures and recoveries are counted separately. The first 10 of each per
`init()` call are logged in detail, in parallel processing order; if a limit is
@@ -563,12 +577,14 @@ last modifier would.
priorities, weak boundaries that coincide or share an edge, enforced
subdivision, whole-perimeter weak zones with painting and priorities, weak
zones over painting's oversampled candidates, the warning type masks, usage
tracking for the "had no effect" warning, volume sorting of strong and weak
groups, restoration of strong points after alignment, raft layer indexing and
structured slices. End-to-end tests slice a real object with Precise Seam
tracking for the "had no effect" and whole-perimeter Blocked warnings, volume
sorting of strong and weak groups, restoration of strong points after
alignment, raft layer indexing and structured slices. End-to-end tests slice
a real object with Precise Seam
volumes and check the outer wall starts in the exported G-code: every strong
mode under several seam positions and with a raft, Enforced and Blocked zones,
a modifier with a hole, and the user warning.
a perimeter blocked all round under every seam position, a modifier with a
hole, and the user warning.
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover
enforced-patch selection independent of the contour start, fully painted
contours, duplicate removal, and `Print::apply()` synchronization through type
+10
View File
@@ -12665,6 +12665,16 @@ msgstr ""
msgid "a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)"
msgstr ""
#. TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name.
#, possible-boost-format
msgid "%1% modifier \"%2%\" of \"%3%\" covers an entire perimeter"
msgstr ""
#. TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name, %4% the number of modifiers.
#, possible-boost-format
msgid "%1% modifier \"%2%\" of \"%3%\" (%4% in total) covers an entire perimeter"
msgstr ""
#, possible-boost-format
msgid "modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)"
msgstr ""
+20
View File
@@ -1374,6 +1374,26 @@ bool AppConfig::write_config_file(const std::string &path, std::string body, con
return true;
}
void AppConfig::clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id)
{
// Unscoped credentials predate printer agents and belong to the legacy BBL agent.
const bool is_bbl_agent = agent_id.empty() || agent_id == "bbl";
auto it = m_local_machines.find(dev_id);
if (it != m_local_machines.end()) {
const std::string& stored_agent = it->second.printer_agent_id;
if (stored_agent == agent_id || (is_bbl_agent && (stored_agent.empty() || stored_agent == "bbl"))) {
if (!it->second.access_code.empty()) {
it->second.access_code.clear();
m_dirty = true;
}
}
}
if (is_bbl_agent) {
erase("access_code", dev_id);
erase("user_access_code", dev_id);
}
}
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
{
const auto it_v = m_vendors.find(vendor);
+2
View File
@@ -306,6 +306,8 @@ public:
}
}
void clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id);
const std::vector<std::string> &get_filament_presets() const { return m_filament_presets; }
void set_filament_presets(const std::vector<std::string> &filament_presets){
m_filament_presets = filament_presets;
+6 -3
View File
@@ -9776,7 +9776,10 @@ bool has_restore_data(std::string & path, std::string& origin)
origin = "<lock>";
return false;
}
if (boost::filesystem::exists(path + "/lock.txt")) {
// ORCA: non-throwing probes; ec is deliberately unread, a backup path that cannot be
// probed is treated as having no restore data rather than aborting the application.
boost::system::error_code ec;
if (boost::filesystem::exists(path + "/lock.txt", ec)) {
std::string pid;
load_string_file(path + "/lock.txt", pid);
try {
@@ -9791,10 +9794,10 @@ bool has_restore_data(std::string & path, std::string& origin)
}
}
std::string file3mf = path + "/.3mf";
if (!boost::filesystem::exists(file3mf))
if (!boost::filesystem::exists(file3mf, ec))
return false;
try {
if (boost::filesystem::exists(path + "/origin.txt"))
if (boost::filesystem::exists(path + "/origin.txt", ec))
load_string_file(path + "/origin.txt", origin);
}
catch (...) {
+23 -19
View File
@@ -673,7 +673,7 @@ static void refine_at_vertex(
}
}
// Records one extraction for the warnings: the type of a modifier with a discarded fragment, and the
// Records one extraction for the warnings: the type of a modifier with a discarded fragment, the
// evaluated/reached flags for "had no effect" (a discarded fragment counts as reached, a contact not).
static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume *modifier,
const SegmentExtraction &extracted)
@@ -685,13 +685,10 @@ static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume *
const auto it = warnings->modifier_usage.find(modifier);
if (it == warnings->modifier_usage.end())
return;
// Load before store: most calls find the flag already set, so shared cache lines stay clean.
PreciseSeamWarnings::ModifierUsage &usage = it->second;
if (!usage.checked.load(std::memory_order_relaxed))
usage.checked.store(true, std::memory_order_relaxed);
const bool reached = !extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0;
if (reached && !usage.reached.load(std::memory_order_relaxed))
usage.reached.store(true, std::memory_order_relaxed);
PreciseSeamWarnings::set(usage.checked);
if (!extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0)
PreciseSeamWarnings::set(usage.reached);
}
// Strong priority is per modifier, never a global maximum across different modifiers.
@@ -953,22 +950,15 @@ std::vector<WeakModifierSegment> collect_weak_modifier_segments(
const SegmentExtraction extracted = extract_perimeter_segments(prepared, it->second[layer_id], modifier_volume->type(), context);
record_extraction(warnings, modifier_volume, extracted);
const auto type = convert_weak_modifier_type(modifier_volume->type());
// Full containment works like seam painting: Enforced and Neutral type the whole perimeter;
// Blocked is skipped with a warning, since the seam cannot avoid the whole perimeter.
// Full containment works like seam painting all round: the whole perimeter takes the type.
// A perimeter left whole Blocked also gets a warning (see apply_weak_modifiers_to_perimeter()).
if (extracted.full_containment) {
if (type == EnforcedBlockedSeamPoint::Blocked) {
if (warnings)
PreciseSeamWarnings::mark(warnings->full_containment, modifier_volume->type());
continue;
}
WeakModifierSegment whole{type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}};
whole.whole_perimeter = true;
result.push_back(whole);
result.push_back({type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}, true, modifier_volume});
continue;
}
for (const PerimeterSegment &segment : extracted.segments) {
result.push_back({type, segment.polyline.points.front(), segment.begin,
segment.polyline.points.back(), segment.end});
segment.polyline.points.back(), segment.end, false, modifier_volume});
}
}
// Boundary insertion, refinement and last-write-wins priority use the established path.
@@ -979,7 +969,8 @@ void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced)
bool &some_point_enforced,
PreciseSeamWarnings *warnings)
{
// Get z-coordinate for unscaling boundary points
const float z_coord = result.points[perimeter.start_index].position.z();
@@ -1029,6 +1020,19 @@ void apply_weak_modifiers_to_perimeter(
some_point_enforced = true;
}
}
// Only the highest whole zone is in effect, and a higher band may still unblock part of it.
if (warnings == nullptr)
return;
const auto last_whole = std::find_if(weak_segments.rbegin(), weak_segments.rend(),
[](const WeakModifierSegment &segment) { return segment.whole_perimeter; });
if (last_whole == weak_segments.rend() || last_whole->type != EnforcedBlockedSeamPoint::Blocked)
return;
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i)
if (result.points[i].type != EnforcedBlockedSeamPoint::Blocked)
return;
if (const auto usage = warnings->modifier_usage.find(last_whole->modifier); usage != warnings->modifier_usage.end())
PreciseSeamWarnings::set(usage->second.fully_blocked);
}
// Restore precise seam positions that may have been modified
+20 -10
View File
@@ -49,18 +49,20 @@ struct PreciseSeamWarnings {
// Masks of the Precise Seam types that caused each warning reason, one bit per type (type_bit()).
// The user warning lists the types instead of naming modifiers.
std::atomic<unsigned> multiple_intersections{0}; // Center/Left/Right with several segments on a perimeter.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right, Blocked.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right.
std::atomic<unsigned> failed_types{0}; // Types with at least one discarded fragment.
std::atomic<size_t> failed_fragments{0}; // Total discarded fragments, for the log summary.
// Fragments saved by the rare-case fallback or accepted as contacts; log only, no user warning.
// Clipper is deterministic, so a prismatic model can repeat the same case on every layer.
std::atomic<size_t> recovered_fragments{0};
// Per-modifier flags for the "had no effect" warning. Modifiers are registered before the parallel
// phase, so workers only set flags; unregistered ones (e.g. in tests) are not tracked.
// Per-modifier flags for the "had no effect" and "covers an entire perimeter" warnings. Modifiers are
// registered before the parallel phase, so workers only set flags; unregistered ones (e.g. in tests)
// are not tracked.
struct ModifierUsage {
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> fully_blocked{false}; // Seam Blocked whole zone in effect on a perimeter left Blocked.
};
std::unordered_map<const ModelVolume*, ModifierUsage> modifier_usage;
@@ -70,13 +72,18 @@ struct PreciseSeamWarnings {
assert(is_precise_seam(type));
return 1u << (int(type) - int(ModelVolumeType::PRECISE_SEAM_CENTER));
}
// Load before fetch_or: most calls find the bit already set, so shared cache lines stay clean.
// Load before writing: most calls find the bit or flag already set, so shared cache lines stay clean.
static void mark(std::atomic<unsigned> &mask, ModelVolumeType type)
{
const unsigned bit = type_bit(type);
if ((mask.load(std::memory_order_relaxed) & bit) == 0)
mask.fetch_or(bit, std::memory_order_relaxed);
}
static void set(std::atomic<bool> &flag)
{
if (!flag.load(std::memory_order_relaxed))
flag.store(true, std::memory_order_relaxed);
}
};
// Optional caller identity for concise diagnostics when an intersection is discarded.
@@ -139,9 +146,10 @@ struct WeakModifierSegment {
PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement.
Point right_point; // Coordinates of right (last) point of segment
PerimeterPosition right_position; // Retained provenance, not an index into the modified polygon.
// Full containment of an Enforced or Neutral modifier: the zone is the whole perimeter, without
// boundaries (the points and positions above are unused and nothing is inserted for it).
// Full containment of a weak modifier: the zone is the whole perimeter, without boundaries
// (the points and positions above are unused and nothing is inserted for it).
bool whole_perimeter = false;
const ModelVolume *modifier = nullptr; // Source modifier, for the warnings.
};
// Collects the object's Precise Seam volumes: strong ones in priority order, weak ones in application
@@ -173,12 +181,14 @@ std::vector<WeakModifierSegment> collect_weak_modifier_segments(
PreciseSeamWarnings* warnings = nullptr);
// Retypes the candidates inside each zone in the given order (pass zones lowest priority first);
// sets some_point_enforced when an Enforced zone applies.
// sets some_point_enforced when an Enforced zone applies. When the whole perimeter ends up Blocked and
// the highest whole-perimeter zone is Blocked, flags that zone's modifier for the warning.
void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced);
bool &some_point_enforced,
PreciseSeamWarnings *warnings = nullptr);
// Restore precise seam positions that may have been modified by alignment
// Iterates through all perimeters and restores precise_seam_point positions
+41 -20
View File
@@ -608,7 +608,7 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
// Apply weak modifiers if no strong modifier was inserted
if (!inserted_seam_position.has_value() && !weak_segments.empty()) {
PreciseSeam::apply_weak_modifiers_to_perimeter(
weak_segments, result, perimeter, some_point_enforced);
weak_segments, result, perimeter, some_point_enforced, warnings);
}
if (some_point_enforced) {
@@ -896,18 +896,15 @@ struct SeamComparator {
return a.central_enforcer;
}
if (a.type == EnforcedBlockedSeamPoint::Enforced) {
return true;
}
if (a.type == EnforcedBlockedSeamPoint::Blocked) {
return false;
}
if (a.type != b.type) {
return a.type > b.type;
}
// Two Blocked points fall through like Neutral ones, so a perimeter blocked all round still gets a proper seam.
if (a.type == EnforcedBlockedSeamPoint::Enforced) {
return true;
}
//avoid overhangs
if ((a.overhang > 0.0f || b.overhang > 0.0f)
&& abs(a.overhang - b.overhang) > (0.1f * a.perimeter.flow_width)) {
@@ -1660,18 +1657,42 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
parts.push_back((boost::format(_u8L("multiple intersections with a perimeter, only one was used (%1%)")) % type_list(mi)).str());
if (fc != 0)
parts.push_back((boost::format(_u8L("a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)")) % type_list(fc)).str());
// Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported.
// Print and volume order make the named one deterministic; the log lists them all.
std::vector<const ModelVolume*> no_effect;
for (const PrintObject *po : print.objects())
for (const ModelVolume *volume : po->model_object()->volumes) {
const auto it = precise_seam_warnings.modifier_usage.find(volume);
if (it != precise_seam_warnings.modifier_usage.end() &&
it->second.checked.load(std::memory_order_relaxed) &&
!it->second.reached.load(std::memory_order_relaxed) &&
std::find(no_effect.begin(), no_effect.end(), volume) == no_effect.end())
no_effect.push_back(volume);
// Modifiers whose usage flags match, in print and volume order, so the named one is deterministic.
const auto collect_modifiers = [&print, &precise_seam_warnings](const auto &matches) {
std::vector<const ModelVolume*> found;
for (const PrintObject *po : print.objects())
for (const ModelVolume *volume : po->model_object()->volumes) {
const auto it = precise_seam_warnings.modifier_usage.find(volume);
if (it != precise_seam_warnings.modifier_usage.end() && matches(it->second) &&
std::find(found.begin(), found.end(), volume) == found.end())
found.push_back(volume);
}
return found;
};
// Seam Blocked modifiers whose whole perimeter stayed Blocked; the user warning names only the
// first one, the log lists them all.
const std::vector<const ModelVolume*> fully_blocked = collect_modifiers(
[](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) { return usage.fully_blocked.load(std::memory_order_relaxed); });
for (const ModelVolume *volume : fully_blocked)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFullyBlocked] object=\"" << volume->get_object()->name
<< "\" modifier=\"" << volume->name << "\"";
if (!fully_blocked.empty()) {
const ModelVolume *first = fully_blocked.front();
if (fully_blocked.size() == 1) {
// TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name.
parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" covers an entire perimeter"))
% _u8L("Seam Blocked") % first->name % first->get_object()->name).str());
} else {
// TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name, %4% the number of modifiers.
parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" (%4% in total) covers an entire perimeter"))
% _u8L("Seam Blocked") % first->name % first->get_object()->name % fully_blocked.size()).str());
}
}
// Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported.
const std::vector<const ModelVolume*> no_effect = collect_modifiers(
[](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) {
return usage.checked.load(std::memory_order_relaxed) && !usage.reached.load(std::memory_order_relaxed);
});
// The user warning names only the first one; the log lists them all.
for (const ModelVolume *volume : no_effect)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamNoEffect] object=\"" << volume->get_object()->name
+9 -3
View File
@@ -2235,9 +2235,11 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
//FIXME It is quite expensive to generate object layers just to get the print height!
auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
if (!layers.empty()) {
effective_max_z = layers.back();
// The object layers start above the raft.
const double raft_z = print_object.slicing_parameters().object_print_z_min;
effective_max_z = raft_z + layers.back();
last_layer_below_max = layers.size() >= 2 &&
0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
raft_z + 0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
have_height = true;
}
}
@@ -2245,7 +2247,11 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (!have_height)
continue;
if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The layers of a regular printer already carry the Z shrinkage compensation, the belt height does not.
const double compensated_max_z = belt_printer ? effective_max_z * shrinkage_compensation_z : effective_max_z;
const double uncompensated_max_z = belt_printer ? effective_max_z : print_object.slicing_parameters().object_print_z_uncompensated_max;
if (shrinkage_compensation_z != 1. && compensated_max_z > this->config().printable_height + EPSILON &&
uncompensated_max_z <= this->config().printable_height + EPSILON) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
+2 -1
View File
@@ -1,5 +1,6 @@
#include "ConnectPrinter.hpp"
#include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
@@ -169,7 +170,7 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
{
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
if (code.empty())
if (code.empty() && (!m_obj || m_obj->printer_agent_id != MOONRAKER_PRINTER_AGENT_ID))
code = "88888888";
for (char c : code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
+12
View File
@@ -276,6 +276,18 @@ namespace Slic3r
std::string connect_type = j["connect_type"].get<std::string>();
std::string bind_state = j["bind_state"].get<std::string>();
// Passive discovery must not replace another agent's saved binding.
if (get_current_printer_agent_id() == MOONRAKER_PRINTER_AGENT_ID) {
if (AppConfig* config = get_app_config()) {
const auto& machines = config->get_local_machines();
auto it = machines.find(dev_id);
if (it != machines.end() && it->second.printer_agent_id != MOONRAKER_PRINTER_AGENT_ID) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring Moonraker discovery for a device owned by another agent: " << dev_id;
return;
}
}
}
if (connect_type == "farm") {
connect_type ="lan";
bind_state = "free";
+28 -20
View File
@@ -4,6 +4,7 @@
#include "HMS.hpp"
#include "I18N.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
@@ -66,6 +67,7 @@
#include <vector>
#include <wx/colour.h>
#include <tuple>
#include <wx/app.h>
#include <wx/dir.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -527,6 +529,18 @@ bool MachineObject::HasRecentLanMessage()
return diff.count() < 5000;
}
bool MachineObject::has_access_right() const
{
return !m_access_revoked && (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID || !get_access_code().empty());
}
void MachineObject::revoke_access()
{
// UI panels retain this object; revoke its binding without invalidating their pointers.
access_code.clear();
m_access_revoked = true;
}
std::string MachineObject::get_access_code() const
{
return access_code;
@@ -535,6 +549,7 @@ std::string MachineObject::get_access_code() const
void MachineObject::set_access_code(std::string code, bool only_refresh)
{
this->access_code = code;
m_access_revoked = false;
if (only_refresh) {
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
if (config) {
@@ -548,25 +563,10 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
// session boundary, since parse_user_print_info() always overwrites their code
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
if (has_access_right()) {
DeviceManager::update_local_machine(*this, config);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
const auto& machines = config->get_local_machines();
auto it = machines.find(get_dev_id());
if (it != machines.end()) {
BBLocalMachine local_machine = it->second;
local_machine.access_code = "";
config->update_local_machine(local_machine);
}
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
// old BBL-era code can't silently "re-bind" this device again via
// get_access_code_with_legacy_fallback()'s legacy fallback.
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
config->erase("access_code", get_dev_id());
config->erase("user_access_code", get_dev_id());
}
config->clear_local_machine_access_code(get_dev_id(), printer_agent_id);
}
} else {
if (!code.empty())
@@ -2678,7 +2678,7 @@ void MachineObject::update_print_progress(const json& value)
int MachineObject::connect()
{
if (get_dev_ip().empty()) return -1;
if (m_access_revoked || get_dev_ip().empty()) return -1;
std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code();
@@ -2691,17 +2691,25 @@ int MachineObject::connect()
std::string host = Http::get_host_from_url(input, &port);
std::string ca_file;
if (GUI::wxGetApp().preset_bundle) {
if (wxTheApp && GUI::wxGetApp().preset_bundle) {
const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (port.empty())
port = config.opt_string("printhost_port");
ca_file = config.opt_string("printhost_cafile");
// Refresh credentials from the matching preset for connections that bypass GUI_App::select_machine().
const std::string print_host = config.opt_string("print_host");
if (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID && config.opt_string("printer_agent") == MOONRAKER_PRINTER_AGENT_ID &&
!print_host.empty() && dev_id_from_address(print_host, config.opt_string("printhost_port")) == get_dev_id()) {
password = config.opt_string("printhost_apikey");
if (password != get_access_code())
set_access_code(password);
}
}
if (host.empty())
host = get_dev_ip();
if (m_agent) {
try {
PrinterConnectionParams params{
+3 -1
View File
@@ -119,6 +119,7 @@ private:
std::string dev_name;
std::string dev_ip;
std::string access_code;
bool m_access_revoked = false;
// type, time stamp, delay
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
@@ -229,7 +230,8 @@ public:
std::string dev_connection_name; /* lan | eth */
/*access code*/
bool has_access_right() const { return !get_access_code().empty(); }
bool has_access_right() const;
void revoke_access();
std::string get_access_code() const;
void set_access_code(std::string code, bool only_refresh = true);
+21 -25
View File
@@ -1149,14 +1149,8 @@ void GUI_App::post_init()
if(!m_networking_need_update && m_agent) {
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -1814,14 +1808,8 @@ void GUI_App::restart_networking()
init_networking_callbacks();
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -2618,7 +2606,10 @@ void GUI_App::init_download_path()
}
else {
fs::path dp(down_path);
if (!fs::exists(dp)) {
// ORCA: non-throwing probe; ec is deliberately unread, a download_path that cannot be
// probed falls back to the default like a missing one instead of failing startup.
boost::system::error_code ec;
if (!fs::exists(dp, ec)) {
std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data();
app_config->set("download_path", user_down_path);
@@ -4231,8 +4222,10 @@ void GUI_App::switch_printer_agent()
const std::string print_host = config.opt_string("print_host");
if (!print_host.empty()) {
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
if (!sel || sel->get_dev_id() != dev_id)
MachineObject* sel = m_device_manager->get_selected_machine();
const bool access_code_changed = effective_agent_id == MOONRAKER_PRINTER_AGENT_ID && sel &&
sel->get_access_code() != config.opt_string("printhost_apikey");
if (!sel || sel->get_dev_id() != dev_id || access_code_changed)
select_machine(effective_agent_id);
}
}
@@ -4274,6 +4267,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
std::string port = host_cfg->opt_string("printhost_port");
std::string access_code = host_cfg->opt_string("printhost_apikey");
if (access_code.empty() && agent_id != MOONRAKER_PRINTER_AGENT_ID)
access_code = "88888888";
// Generate dev_id from host and port
std::string dev_id = MachineObject::dev_id_from_address(print_host, port);
@@ -4300,11 +4296,6 @@ void GUI_App::select_machine(const std::string& agent_id)
machine.dev_ip = dev_id;
machine.dev_name = dev_id;
machine.printer_type = preset.config.opt_string("printer_model");
auto access_code = preset.config.opt_string("printhost_apikey");
// Orca expect non empty access code
if (access_code.empty()) {
access_code = "88888888";
}
existing = m_device_manager->insert_local_device(
machine, "lan", "free", "", access_code);
@@ -4314,6 +4305,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id;
} else if (agent_id == MOONRAKER_PRINTER_AGENT_ID && existing->printer_agent_id == agent_id &&
(!existing->has_access_right() || existing->get_access_code() != access_code)) {
existing->set_access_code(access_code);
}
existing->local_use_ssl = boost::istarts_with(print_host, "https://");
@@ -10141,7 +10135,9 @@ void GUI_App::start_download(std::string url)
}
//lets always init so if the download dest folder was changed, new dest is used
boost::filesystem::path dest_folder(app_config->get("download_path"));
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) {
// ORCA: non-throwing probe, as in init_download_path(); ec is deliberately unread.
boost::system::error_code ec;
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder, ec)) {
std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard.");
BOOST_LOG_TRIVIAL(error) << msg;
show_error(nullptr, msg);
+2 -1
View File
@@ -732,10 +732,11 @@ void SelectMachinePopup::update_user_devices()
if (mobj) {
AppConfig* config = wxGetApp().app_config;
if (config) {
config->clear_local_machine_access_code(mobj->get_dev_id(), mobj->printer_agent_id);
config->erase_local_machine(mobj->get_dev_id());
}
mobj->set_access_code("");
mobj->revoke_access();
}
if (GUI::wxGetApp().plater())
+6 -6
View File
@@ -2981,7 +2981,7 @@ void TabPrint::build()
optgroup->append_single_option_line("skeleton_infill_line_width", "strength_settings_patterns#locked-zag");
optgroup->append_single_option_line("symmetric_infill_y_axis", "strength_settings_infill#symmetric-infill-y-axis");
optgroup->append_single_option_line("infill_complete_top", "strength_settings_patterns#fill-pattern-tops");
optgroup->append_single_option_line("infill_shift_step", "strength_settings_patterns#cross-hatch");
optgroup->append_single_option_line("infill_shift_step", "strength_settings_patterns#cross-zag");
optgroup->append_single_option_line("lateral_lattice_angle_1", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("lateral_lattice_angle_2", "strength_settings_patterns#lateral-lattice");
optgroup->append_single_option_line("infill_overhang_angle", "strength_settings_patterns#lateral-honeycomb");
@@ -3248,8 +3248,8 @@ void TabPrint::build()
optgroup->append_single_option_line("enable_wrapping_detection");
optgroup = page->new_optgroup(L("Fuzzy skin"), L"fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode");
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin#fuzzy-skin-mode");
optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-generator-mode");
optgroup->append_single_option_line("fuzzy_skin_noise_type", "others_settings_fuzzy_skin#noise-type");
optgroup->append_single_option_line("fuzzy_skin_point_distance", "others_settings_fuzzy_skin#point-distance");
optgroup->append_single_option_line("fuzzy_skin_thickness", "others_settings_fuzzy_skin#skin-thickness");
@@ -3283,7 +3283,7 @@ void TabPrint::build()
option.opt.full_width = true;
option.opt.is_code = true;
option.opt.height = 15;
optgroup->append_single_option_line(option);
optgroup->append_single_option_line(option, "others_settings_g_code_output#change-extrusion-role-g-code");
optgroup = page->new_optgroup(L("Post-processing Scripts"), L"param_gcode", 0);
option = optgroup->get_option("post_process");
@@ -3296,7 +3296,7 @@ void TabPrint::build()
optgroup->hide_labels();
option = optgroup->get_option("slicing_pipeline_plugin");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
optgroup->append_single_option_line(option, "plugins_types#slicing-pipeline");
// Its own group: the one above hides its labels, and this row needs its label — and the revert
// arrow beside it — to show. No label-width override either, as a 0 there means "no label column".
@@ -3547,7 +3547,7 @@ void TabPrintModel::build()
auto optgroup = page->new_optgroup("");
optgroup->append_single_option_line("layer_height", "quality_settings_layer_height");
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("wall_loops", "strength_settings_walls");
optgroup->append_single_option_line("wall_loops", "strength_settings_walls#wall-loops");
optgroup->append_single_option_line("enable_support", "support_settings_support");
m_pages.pop_back();
m_pages.insert(m_pages.begin(), page);
-1
View File
@@ -61,7 +61,6 @@ struct CurlGlobalInit
"/etc/ssl/ca-bundle.pem" // OpenSUSE Tumbleweed
};
namespace fs = boost::filesystem;
// Env var name for the OpenSSL CA bundle (SSL_CERT_FILE nomally)
const char *const SSL_CA_FILE = X509_get_default_cert_file_env();
const char * ssl_cafile = ::getenv(SSL_CA_FILE);
-3
View File
@@ -52,9 +52,6 @@ std::string Moonraker::make_url(const std::string &path) const
void Moonraker::set_auth(Http &http) const
{
//ORCA: Moonraker accepts unauthenticated requests by default; X-Api-Key is the only auth header
// defined by the Moonraker spec. HTTP Basic / Digest do NOT belong here even if the user
// filled the user/password fields — those are PrusaLink/OctoPrint conventions.
if (!m_apikey.empty())
http.header("X-Api-Key", m_apikey);
if (!m_cafile.empty())
+4 -3
View File
@@ -27,9 +27,10 @@ class Http;
// plateindex)
// POST /printer/print/start (json) -- {"filename":"<name>.gcode"} starts print
//
// Auth: X-Api-Key header if `printhost_apikey` is non-empty; Moonraker accepts
// unauthenticated LAN access by default, so the key is optional. HTTP Basic /
// Digest are not part of the Moonraker spec and are not sent.
// Auth: Send X-Api-Key when `printhost_apikey` is non-empty. Without a key,
// requests rely on Moonraker's `[authorization] trusted_clients`; `force_logins`
// overrides trusted-client access when enabled and at least one user exists.
// HTTP Basic / Digest credentials are not sent.
class Moonraker : public PrintHost
{
public:
+3 -7
View File
@@ -1,6 +1,7 @@
#include "MoonrakerPrinterAgent.hpp"
#include "Http.hpp"
#include "IPrinterAgent.hpp"
#include "NetworkAgentFactory.hpp"
#include "bambu_networking.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -142,7 +143,7 @@ MoonrakerPrinterAgent::~MoonrakerPrinterAgent()
AgentInfo MoonrakerPrinterAgent::get_agent_info_static()
{
return AgentInfo{"moonraker", "Moonraker", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"};
return AgentInfo{MOONRAKER_PRINTER_AGENT_ID, "Moonraker", MoonrakerPrinterAgent_VERSION, "Klipper/Moonraker printer agent"};
}
void MoonrakerPrinterAgent::set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud)
@@ -1372,7 +1373,7 @@ void MoonrakerPrinterAgent::announce_printhost_device()
{
std::lock_guard<std::recursive_mutex> lock(state_mutex);
ssdp_fn = on_ssdp_msg_fn;
if (!ssdp_fn) {
if (!ssdp_fn || device_info.dev_id.empty() || device_info.dev_ip.empty() || device_info.base_url.empty()) {
return;
}
if (ssdp_announced_host == device_info.base_url && !ssdp_announced_id.empty()) {
@@ -1393,11 +1394,6 @@ void MoonrakerPrinterAgent::announce_printhost_device()
const std::string model_id = device_info.model_id;
if (auto* app_config = GUI::wxGetApp().app_config) {
const std::string access_code = device_info.api_key.empty() ? "88888888" : device_info.api_key;
app_config->set_str("access_code", device_info.dev_id, access_code);
}
nlohmann::json payload;
payload["dev_name"] = dev_name;
payload["dev_id"] = device_info.dev_id;
+28 -5
View File
@@ -128,8 +128,8 @@ void NetworkAgent::add_cloud_agent(const std::string& provider, std::shared_ptr<
void NetworkAgent::set_printer_agent(std::shared_ptr<IPrinterAgent> printer_agent)
{
m_user_machine_list_generation.fetch_add(1);
// Disconnect all callbacks from the old agent
// Invalidate queued discovery even when a cached agent instance is reused.
m_discovery_session = std::make_shared<int>(0);
auto old_printer_agent = m_printer_agent;
m_printer_agent = std::move(printer_agent);
@@ -155,7 +155,7 @@ void NetworkAgent::apply_printer_callbacks(const std::shared_ptr<IPrinterAgent>&
return;
}
printer_agent->set_on_ssdp_msg_fn(callbacks.on_ssdp_msg_fn);
apply_discovery_callback(printer_agent, callbacks);
printer_agent->set_on_printer_connected_fn(callbacks.on_printer_connected_fn);
printer_agent->set_on_subscribe_failure_fn(callbacks.on_subscribe_failure_fn);
printer_agent->set_on_message_fn(callbacks.on_message_fn);
@@ -166,6 +166,26 @@ void NetworkAgent::apply_printer_callbacks(const std::shared_ptr<IPrinterAgent>&
printer_agent->set_server_callback(callbacks.on_server_err_fn);
}
int NetworkAgent::apply_discovery_callback(const std::shared_ptr<IPrinterAgent>& printer_agent,
const PrinterCallbacks& callbacks)
{
if (!callbacks.on_ssdp_msg_fn)
return printer_agent->set_on_ssdp_msg_fn(nullptr);
const std::weak_ptr<int> session = m_discovery_session;
return printer_agent->set_on_ssdp_msg_fn(
[session, fn = callbacks.on_ssdp_msg_fn, queue = callbacks.queue_on_main_fn](std::string payload) {
// Without a UI queue this would run on the agent's thread, so drop it.
if (session.expired() || !queue)
return;
queue([session, fn, payload = std::move(payload)] {
// The agent may have changed while this event waited on the UI queue.
if (!session.expired())
fn(payload);
});
});
}
std::shared_ptr<ICloudServiceAgent> NetworkAgent::get_cloud_agent(const std::string& provider) const
{
const auto& key = (provider.empty() || provider == ORCA_CLOUD_PROVIDER) ? ORCA_CLOUD_PROVIDER : provider;
@@ -185,8 +205,10 @@ int NetworkAgent::set_queue_on_main_fn(QueueOnMainFn fn, const std::string& prov
int ret = -1;
if (cloud_agent)
ret = cloud_agent->set_queue_on_main_fn(fn);
if (m_printer_agent)
if (m_printer_agent) {
m_printer_agent->set_queue_on_main_fn(fn);
apply_discovery_callback(m_printer_agent, m_printer_callbacks);
}
return ret;
}
@@ -737,9 +759,10 @@ int NetworkAgent::get_mw_user_4ulist(int seed, int limit, std::function<void(std
int NetworkAgent::set_on_ssdp_msg_fn(OnMsgArrivedFn fn)
{
m_discovery_session = std::make_shared<int>(0);
m_printer_callbacks.on_ssdp_msg_fn = fn;
if (m_printer_agent)
return m_printer_agent->set_on_ssdp_msg_fn(fn);
return apply_discovery_callback(m_printer_agent, m_printer_callbacks);
return -1;
}
+4
View File
@@ -144,6 +144,7 @@ public:
int get_mw_user_4ulist(int seed, int limit, std::function<void(std::string)> callback, const std::string& provider = ORCA_CLOUD_PROVIDER);
// Printer agent methods
// Discovery is delivered through queue_on_main_fn, scoped to the live agent session.
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn);
int set_on_printer_connected_fn(OnPrinterConnectedFn fn);
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn);
@@ -210,6 +211,9 @@ private:
void apply_printer_callbacks(const std::shared_ptr<IPrinterAgent>& printer_agent,
const PrinterCallbacks& callbacks);
int apply_discovery_callback(const std::shared_ptr<IPrinterAgent>& printer_agent,
const PrinterCallbacks& callbacks);
std::shared_ptr<int> m_discovery_session = std::make_shared<int>(0);
PrinterCallbacks m_printer_callbacks;
bool enable_track = false;
+1
View File
@@ -18,6 +18,7 @@
namespace Slic3r {
static constexpr char ORCA_PRINTER_AGENT_ID[] = "orca";
static constexpr char BBL_PRINTER_AGENT_ID[] = "bbl";
static constexpr char MOONRAKER_PRINTER_AGENT_ID[] = "moonraker";
// Factory function type for creating printer agents
using PrinterAgentFactory =
+111 -33
View File
@@ -105,7 +105,8 @@ void require_vertex(const Polygon &polygon, const Point &point)
std::vector<SeamPlacerImpl::EnforcedBlockedSeamPoint> weak_candidate_types(
const Polygon &polygon, const std::vector<PreciseSeam::WeakModifierSegment> &segments,
const std::function<SeamPlacerImpl::EnforcedBlockedSeamPoint(const Point &)> &painted = {})
const std::function<SeamPlacerImpl::EnforcedBlockedSeamPoint(const Point &)> &painted = {},
PreciseSeam::PreciseSeamWarnings *warnings = nullptr)
{
// Use the same coordinate conversion as production when applying prepared boundaries.
// Optional painted types stand in for seam painting, which production assigns before weak zones.
@@ -120,7 +121,7 @@ std::vector<SeamPlacerImpl::EnforcedBlockedSeamPoint> weak_candidate_types(
}
loop.end_index = candidates.points.size();
bool enforced = false;
PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced);
PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced, warnings);
std::vector<SeamPlacerImpl::EnforcedBlockedSeamPoint> types;
for (const auto &candidate : candidates.points)
types.push_back(candidate.type);
@@ -664,7 +665,7 @@ TEST_CASE("Weak priority and enforcement refinement apply on both crossed sides"
check_square_boundary(perimeter);
}
TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the whole perimeter, Blocked is skipped", "[PreciseSeam]")
TEST_CASE("Weak full containment follows painting: the whole perimeter takes the zone's type", "[PreciseSeam]")
{
const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED,
ModelVolumeType::PRECISE_SEAM_NEUTRAL);
@@ -674,6 +675,8 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the
const Points original = perimeter.points;
const auto *modifier = fixture.add(type, {rectangle(-2, -2, 22, 22)});
PreciseSeam::PreciseSeamWarnings warnings;
// Registered as SeamPlacer::init() does, so the per-modifier warning flag is tracked.
warnings.modifier_usage.try_emplace(modifier);
const auto segments = PreciseSeam::collect_weak_modifier_segments(
{modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings);
// Painting of both kinds, so that each type's effect on it is visible.
@@ -681,23 +684,17 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the
return p.x() < scale_(10.) ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced :
SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked;
};
const auto types = weak_candidate_types(perimeter, segments, painted);
const auto types = weak_candidate_types(perimeter, segments, painted, &warnings);
CHECK_FALSE(warnings.multiple_intersections.load());
CHECK(warnings.failed_fragments.load() == 0);
if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) {
// Forbidding the seam all round cannot be honoured: skipped with a warning, painting stays.
CHECK(segments.empty());
CHECK(perimeter.points == original);
CHECK(warnings.full_containment.load() == PreciseSeam::PreciseSeamWarnings::type_bit(type));
for (size_t i = 0; i < perimeter.size(); ++i)
CHECK(types[i] == painted(perimeter[i]));
return;
}
REQUIRE(segments.size() == 1);
CHECK(segments.front().whole_perimeter);
CHECK(warnings.full_containment.load() == 0);
// Only a whole Blocked perimeter is reported, as the seam cannot avoid it.
CHECK(warnings.modifier_usage.at(modifier).fully_blocked.load() == (type == ModelVolumeType::PRECISE_SEAM_BLOCKED));
// Every candidate takes the zone's type, overriding painting.
const auto expected = type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced :
type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked :
SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral;
for (size_t i = 0; i < perimeter.size(); ++i)
CHECK(types[i] == expected);
@@ -706,29 +703,37 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the
CHECK(perimeter.size() >= size_t(80.f / SeamPlacer::enforcer_oversampling_distance));
check_square_boundary(perimeter);
} else
CHECK(perimeter.points == original); // Like an unmarked perimeter: no subdivision.
CHECK(perimeter.points == original); // Like a perimeter unmarked or painted red all round: no subdivision.
}
TEST_CASE("Whole-perimeter weak zones take part in the usual priority order", "[PreciseSeam]")
{
// Volumes are listed low priority first; a later zone overwrites an earlier one.
const int scenario = GENERATE(0, 1, 2);
const int scenario = GENERATE(0, 1, 2, 3, 4, 5, 6);
CAPTURE(scenario);
SeamFixture fixture;
const auto *whole_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(-2, -2, 22, 22)});
const auto *whole_neutral = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(-2, -2, 22, 22)});
const auto *whole_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(-2, -2, 22, 22)});
const auto *lower_whole_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(-2, -2, 22, 22)});
const auto *band_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(8, -2, 12, 2)});
const auto *band_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(8, -2, 12, 2)});
std::vector<const ModelVolume*> volumes;
if (scenario == 0) volumes = {whole_enforced, band_blocked}; // A band forbidden inside a green perimeter.
if (scenario == 1) volumes = {band_enforced, whole_neutral}; // A whole Neutral clears the lower band.
if (scenario == 2) volumes = {band_enforced, whole_blocked}; // A whole Blocked is skipped; the band stays.
if (scenario == 2) volumes = {band_enforced, whole_blocked}; // A whole Blocked overrides the lower band.
if (scenario == 3) volumes = {whole_blocked, band_enforced}; // A higher band unblocks part of a whole Blocked.
if (scenario == 4) volumes = {whole_blocked, band_blocked}; // A higher Blocked band keeps it whole Blocked.
if (scenario == 5) volumes = {lower_whole_blocked, whole_enforced, whole_blocked}; // The lower one is overwritten.
if (scenario == 6) volumes = {lower_whole_blocked, whole_blocked}; // Only the higher one is in effect.
Polygon perimeter = rectangle(0, 0, 20, 20);
PreciseSeam::PreciseSeamWarnings warnings;
// Registered as SeamPlacer::init() does, so the per-modifier warning flag is tracked.
warnings.modifier_usage.try_emplace(whole_blocked);
warnings.modifier_usage.try_emplace(lower_whole_blocked);
const auto segments = PreciseSeam::collect_weak_modifier_segments(
volumes, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings);
const auto types = weak_candidate_types(perimeter, segments);
const auto types = weak_candidate_types(perimeter, segments, {}, &warnings);
using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint;
for (size_t i = 0; i < perimeter.size(); ++i) {
const Point &p = perimeter[i];
@@ -736,14 +741,38 @@ TEST_CASE("Whole-perimeter weak zones take part in the usual priority order", "[
const bool in_band = p.y() == 0 && p.x() >= scale_(8.) && p.x() <= scale_(12.);
const Type expected = scenario == 0 ? (in_band ? Type::Blocked : Type::Enforced) :
scenario == 1 ? Type::Neutral :
(in_band ? Type::Enforced : Type::Neutral);
scenario == 3 ? (in_band ? Type::Enforced : Type::Blocked) :
Type::Blocked;
CHECK(types[i] == expected);
}
CHECK(warnings.full_containment.load() ==
(scenario == 2 ? PreciseSeam::PreciseSeamWarnings::type_bit(ModelVolumeType::PRECISE_SEAM_BLOCKED) : 0u));
CHECK(warnings.full_containment.load() == 0);
// The warning follows the final typing: only a perimeter left whole Blocked is reported, through the
// highest whole zone.
const bool expected_fully_blocked = scenario == 2 || scenario == 4 || scenario == 5 || scenario == 6;
CHECK(warnings.modifier_usage.at(whole_blocked).fully_blocked.load() == expected_fully_blocked);
CHECK_FALSE(warnings.modifier_usage.at(lower_whole_blocked).fully_blocked.load());
check_square_boundary(perimeter);
}
TEST_CASE("A partial Blocked zone on a perimeter painted red all round gives no whole-perimeter warning", "[PreciseSeam]")
{
SeamFixture fixture;
const auto *band_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(8, -2, 12, 2)});
Polygon perimeter = rectangle(0, 0, 20, 20);
PreciseSeam::PreciseSeamWarnings warnings;
warnings.modifier_usage.try_emplace(band_blocked);
const auto segments = PreciseSeam::collect_weak_modifier_segments(
{band_blocked}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings);
REQUIRE(segments.size() == 1);
// Red painting makes every candidate Blocked, so the check reaches the whole-zone filter.
const auto painted = [](const Point &) { return SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked; };
const auto types = weak_candidate_types(perimeter, segments, painted, &warnings);
for (const auto type : types)
CHECK(type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked);
// Only a modifier that covers the whole perimeter is reported.
CHECK_FALSE(warnings.modifier_usage.at(band_blocked).fully_blocked.load());
}
TEST_CASE("Coverage short by a gap under 1 um is full containment while a narrow band keeps its point", "[PreciseSeam][Regression]")
{
// A tip pokes through the bottom edge at x = 10 mm by `depth` nm, so the perimeter line crosses it
@@ -829,19 +858,15 @@ TEST_CASE("A modifier touching the perimeter at one point keeps the full contain
else {
const auto segments = PreciseSeam::collect_weak_modifier_segments(
{modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings);
// Enforced and Neutral type the whole perimeter; Blocked is skipped.
if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED)
CHECK(segments.empty());
else {
REQUIRE(segments.size() == 1);
CHECK(segments.front().whole_perimeter);
}
// A weak modifier gives its type to the whole perimeter.
REQUIRE(segments.size() == 1);
CHECK(segments.front().whole_perimeter);
}
// Only a whole-perimeter Enforced zone subdivides the edges.
if (type != ModelVolumeType::PRECISE_SEAM_ENFORCED)
CHECK(perimeter.points == original);
const bool skipped = is_precise_seam_strong(type) || type == ModelVolumeType::PRECISE_SEAM_BLOCKED;
CHECK(warnings.full_containment.load() == (skipped ? PreciseSeam::PreciseSeamWarnings::type_bit(type) : 0u));
// Only strong types are skipped with the full-containment warning.
CHECK(warnings.full_containment.load() == (is_precise_seam_strong(type) ? PreciseSeam::PreciseSeamWarnings::type_bit(type) : 0u));
CHECK_FALSE(warnings.multiple_intersections.load());
CHECK(warnings.failed_fragments.load() == 0);
}
@@ -1176,7 +1201,7 @@ TEST_CASE("Modifier usage marks evaluated modifiers that never reach a perimeter
CHECK(checked(short_weak));
CHECK_FALSE(reached(short_weak));
CHECK(reached(crossing_weak));
// Full containment reached the perimeter even though it is skipped.
// Full containment counts as reaching the perimeter.
CHECK(reached(containing_weak));
// Strong: evaluation stops at the first modifier with a segment. The one ending 0.5 mm before the
@@ -1393,6 +1418,24 @@ TEST_CASE("Weak modifiers move the exported seam into or out of their zone", "[P
CHECK(fixture.warning().empty());
}
TEST_CASE("A higher-priority Enforced band suppresses the whole-perimeter Blocked export warning", "[PreciseSeam][Regression]")
{
ExportFixture fixture("back");
// The earlier model volume has higher priority; weak application reverses this order.
fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, make_cube(2, 6, 4), Vec3d(4, -3, -1), "front enforcer");
fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 4), Vec3d(-5, -5, -1), "whole blocker");
const auto loops = outer_wall_loops(fixture.export_gcode());
REQUIRE(loops.size() == 10);
for (const OuterLoop &loop : loops) {
CAPTURE(loop.seam.x(), loop.seam.y());
// The band wins on every layer, so the empty warning is not from modifiers that did nothing.
CHECK_THAT(loop.seam.y(), Catch::Matchers::WithinAbs(loop.bounds.min.y() - loop.bounds.center().y(), 0.01));
CHECK(loop.seam.x() >= -6.01);
CHECK(loop.seam.x() <= -3.99);
}
CHECK(fixture.warning().empty());
}
TEST_CASE("A strong modifier with a hole uses the left of two equal crossings", "[PreciseSeam]")
{
// The hole splits the front face crossing into x = 2..4 and x = 8..10 mm. The lengths tie, both
@@ -1422,14 +1465,49 @@ TEST_CASE("Full containment warns for Blocked but not for Enforced", "[PreciseSe
{
const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED);
ExportFixture fixture("back");
fixture.add(type, make_cube(30, 30, 4), Vec3d(-5, -5, -1));
fixture.export_gcode();
fixture.add(type, make_cube(30, 30, 4), Vec3d(-5, -5, -1), "whole helper");
const auto loops = outer_wall_loops(fixture.export_gcode());
// The whole perimeter takes the type either way, so the seam still follows the back preference.
REQUIRE(loops.size() == 10);
for (const OuterLoop &loop : loops)
CHECK(loop.seam.y() > 7.);
if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED)
CHECK(fixture.warning().find("Seam Blocked") != std::string::npos);
CHECK(fixture.warning().find("Seam Blocked modifier \"whole helper\" of") != std::string::npos);
else
CHECK(fixture.warning().empty());
}
TEST_CASE("A perimeter blocked all round gets the seam of an unmarked one", "[PreciseSeam][Regression]")
{
// Equal Blocked candidates compare like Neutral ones, so random and aligned seams still vary and align.
const std::string seam_position = GENERATE(as<std::string>{}, "random", "aligned", "aligned_back", "back", "nearest");
CAPTURE(seam_position);
ExportFixture unmarked(seam_position);
const auto expected = outer_wall_loops(unmarked.export_gcode());
ExportFixture blocked(seam_position);
blocked.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 4), Vec3d(-5, -5, -1));
const auto loops = outer_wall_loops(blocked.export_gcode());
REQUIRE(loops.size() == expected.size());
for (size_t i = 0; i < loops.size(); ++i) {
CAPTURE(i, loops[i].seam.x(), loops[i].seam.y(), expected[i].seam.x(), expected[i].seam.y());
CHECK((loops[i].seam - expected[i].seam).norm() < 0.01);
}
}
TEST_CASE("The whole-perimeter Blocked warning names the first modifier and counts them all", "[PreciseSeam]")
{
ExportFixture fixture("back");
// Separate layers: where both covered the same perimeter, only the higher-priority one would count.
fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 1.4), Vec3d(-5, -5, -1), "first blocker");
fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 2), Vec3d(-5, -5, 1), "second blocker");
fixture.export_gcode();
const std::string warning = fixture.warning();
CAPTURE(warning);
CHECK(warning.find("Seam Blocked modifier \"first blocker\" of") != std::string::npos);
CHECK(warning.find("(2 in total) covers an entire perimeter") != std::string::npos);
CHECK(warning.find("second blocker") == std::string::npos);
}
TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object was sliced in", "[PreciseSeam][belt]")
{
Model model;
+56
View File
@@ -26,6 +26,7 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Print.hpp"
#include "libslic3r/Slicing.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Support/TreeModelVolumes.hpp"
@@ -1596,3 +1597,58 @@ TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt
CHECK(lowest->print_z - floor_under_lowest > -0.2 - EPSILON);
}
}
TEST_CASE("The build height check counts the Z shrinkage compensation once", "[Print][validate][Regression]")
{
// printable_height is 100 mm and Z shrinks to 95 %, so the object is sliced 100 / 95 times taller.
// 92 mm become about 96.8 mm and fit; 97 mm become about 102.1 mm and only the compensation makes them too tall; 101 mm are too tall on their own.
const auto [height, expected] = GENERATE(table<double, std::string>({
{ 92., "" },
{ 97., "because of material shrinkage compensation" },
{ 101., "exceeds the maximum build volume height." },
}));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "printable_height", 100. },
{ "filament_shrinkage_compensation_z", "95%" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "layer_change_gcode", "G92 E0\n" }, // validate() relative-E reset
});
Print print;
Model model;
init_print({ make_cube(20., 20., height) }, print, model, config);
const std::string error = print.validate().string;
INFO(error);
if (expected.empty())
CHECK(error.empty());
else
CHECK(error.find(expected) != std::string::npos);
}
TEST_CASE("The build height check counts the raft", "[Print][validate][Regression]")
{
// printable_height is 100 mm; three raft layers lift the object by 0.9 mm.
const auto [height, fits] = GENERATE(table<double, bool>({ { 98.8, true }, { 99.8, false } }));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "printable_height", 100. },
{ "raft_layers", 3 },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "layer_change_gcode", "G92 E0\n" }, // validate() relative-E reset
});
Print print;
Model model;
init_print({ make_cube(20., 20., height) }, print, model, config);
const SlicingParameters &slicing_params = print.objects().front()->slicing_parameters();
REQUIRE_THAT(slicing_params.object_print_z_min, Catch::Matchers::WithinAbs(0.9, EPSILON));
const std::string error = print.validate().string;
INFO(error);
if (fits)
CHECK(error.empty());
else
CHECK(error.find("maximum build volume height") != std::string::npos);
}
+50
View File
@@ -36,12 +36,18 @@
#include <nlohmann/json.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/file_status.hpp> // no_perms, owner_all
#include <boost/nowide/fstream.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <functional>
#include <initializer_list>
#include <fstream>
#ifndef _WIN32
#include <unistd.h> // geteuid
#endif
#include <catch2/catch_tostring.hpp>
#include <Eigen/Core>
#include <Eigen/Geometry>
@@ -2313,3 +2319,47 @@ SCENARIO("thumbnail relationships are declared only for thumbnails that were wri
}
}
}
// has_restore_data() runs from the queued EVT_RESTORE_PROJECT handler, whose try starts after
// the call, so a filesystem_error from probing the stored backup path aborts the application.
// The realistic trigger is an absolute path from another user's config whose home is not
// searchable, so the parent, not the backup directory, is what blocks.
#ifndef _WIN32
TEST_CASE("has_restore_data reports no restore data for a path it cannot probe", "[3mf]") {
if (::geteuid() == 0)
SKIP("root ignores the directory permissions this test depends on");
ScopedTemporaryDir outer("orca_restore");
boost::filesystem::path backup = outer.path() / "backup";
REQUIRE(boost::filesystem::create_directory(backup));
std::ofstream((backup / ".3mf").string()) << "stand-in for the backed-up project";
std::ofstream((backup / "origin.txt").string()) << "project.3mf";
// Control: while the path is readable the session is found, so the false below really is
// the probe failing and not a mis-built fixture.
std::string readable = backup.string(), origin;
REQUIRE(has_restore_data(readable, origin));
REQUIRE(readable == (backup / ".3mf").string());
REQUIRE(origin == "project.3mf");
// Puts the permissions back before ~ScopedTemporaryDir needs them to clean up, and does so
// even if an assertion below throws.
struct Searchable {
boost::filesystem::path dir;
~Searchable() { boost::system::error_code ec;
boost::filesystem::permissions(dir, boost::filesystem::owner_all, ec); }
} restore{ outer.path() };
// Non-throwing here too: a filesystem that refuses the mode change has to reach the skip
// below rather than fail the test. The probe then still succeeds, which is that same case.
boost::system::error_code probe_ec;
boost::filesystem::permissions(outer.path(), boost::filesystem::no_perms, probe_ec);
if (boost::filesystem::exists(backup / ".3mf", probe_ec) || !probe_ec)
SKIP("the backup path could not be made unprobeable here");
std::string unprobeable = backup.string(), origin2;
bool has = true;
REQUIRE_NOTHROW(has = has_restore_data(unprobeable, origin2));
CHECK_FALSE(has);
CHECK(unprobeable == backup.string()); // not rewritten to the .3mf it could not confirm
}
#endif // _WIN32
+101
View File
@@ -3,6 +3,8 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Thread.hpp"
#include "../slic3rutils/plugin_test_utils.hpp"
using namespace Slic3r;
@@ -46,6 +48,105 @@ TEST_CASE("AppConfig network version helpers", "[AppConfig]") {
}
}
TEST_CASE("Local machine agent replacements survive save and reload", "[AppConfig]")
{
ScopedDataDir data_dir("local-machine");
save_main_thread_id();
AppConfig config;
BBLocalMachine saved;
saved.dev_id = "127.0.0.1:7125";
saved.dev_ip = saved.dev_id;
saved.dev_name = "Test printer";
saved.printer_type = "test-model";
saved.printer_agent_id = "orca";
saved.access_code = "old-key";
config.update_local_machine(saved);
config.save();
REQUIRE_FALSE(config.dirty());
BBLocalMachine replacement = saved;
replacement.printer_agent_id = "moonraker";
replacement.access_code = GENERATE("", "new-key", "88888888");
config.update_local_machine(replacement);
REQUIRE(config.dirty());
REQUIRE(config.get_local_machines().at(saved.dev_id) == replacement);
config.save();
AppConfig reloaded;
REQUIRE(reloaded.load().empty());
REQUIRE(reloaded.get_local_machines().at(saved.dev_id) == replacement);
REQUIRE_FALSE(reloaded.dirty());
reloaded.update_local_machine(replacement);
REQUIRE_FALSE(reloaded.dirty());
}
TEST_CASE("Clearing local credentials preserves another agent's record", "[AppConfig]")
{
AppConfig config;
BBLocalMachine saved;
saved.dev_id = "test_dev";
saved.printer_agent_id = GENERATE("orca", "qidi", "bbl", "");
saved.access_code = "saved-key";
config.update_local_machine(saved);
config.set_str("access_code", saved.dev_id, "legacy-key");
config.set_str("user_access_code", saved.dev_id, "legacy-user-key");
config.clear_local_machine_access_code(saved.dev_id, "moonraker");
REQUIRE(config.get_local_machines().at(saved.dev_id) == saved);
REQUIRE(config.get("access_code", saved.dev_id) == "legacy-key");
REQUIRE(config.get("user_access_code", saved.dev_id) == "legacy-user-key");
}
TEST_CASE("Clearing local credentials changes only the owning agent's key", "[AppConfig]")
{
ScopedDataDir data_dir("local-machine-clear");
save_main_thread_id();
AppConfig config;
BBLocalMachine saved;
saved.dev_id = "test_dev";
saved.dev_name = "Test printer";
saved.printer_agent_id = GENERATE("moonraker", "qidi");
saved.access_code = "saved-key";
config.update_local_machine(saved);
config.save();
REQUIRE_FALSE(config.dirty());
config.clear_local_machine_access_code(saved.dev_id, saved.printer_agent_id);
REQUIRE(config.dirty());
config.save();
AppConfig reloaded;
REQUIRE(reloaded.load().empty());
saved.access_code.clear();
REQUIRE(reloaded.get_local_machines().at(saved.dev_id) == saved);
REQUIRE_FALSE(reloaded.dirty());
reloaded.clear_local_machine_access_code(saved.dev_id, saved.printer_agent_id);
reloaded.clear_local_machine_access_code("unknown_dev", saved.printer_agent_id);
REQUIRE(reloaded.get_local_machines().size() == 1);
REQUIRE_FALSE(reloaded.dirty());
}
TEST_CASE("BBL credential clearing removes legacy fallback without changing foreign records", "[AppConfig]")
{
AppConfig config;
BBLocalMachine saved;
saved.dev_id = "test_dev";
saved.printer_agent_id = GENERATE("bbl", "", "moonraker");
saved.access_code = "saved-key";
config.update_local_machine(saved);
config.set_str("access_code", saved.dev_id, "legacy-key");
config.set_str("user_access_code", saved.dev_id, "legacy-user-key");
config.clear_local_machine_access_code(saved.dev_id, GENERATE("bbl", ""));
if (saved.printer_agent_id != "moonraker")
saved.access_code.clear();
REQUIRE(config.get_local_machines().at(saved.dev_id) == saved);
REQUIRE(config.get("access_code", saved.dev_id).empty());
REQUIRE(config.get("user_access_code", saved.dev_id).empty());
}
TEST_CASE("Remembered checkbox settings retain both selections", "[AppConfig][Regression]") {
AppConfig config;
const bool checked = GENERATE(false, true);
+3
View File
@@ -5,6 +5,9 @@ add_executable(${_TEST_NAME}_tests
test_config_value_formatter.cpp
test_creality_cfs_match.cpp
test_dev_mapping.cpp
test_device_access.cpp
test_moonraker_discovery.cpp
test_network_discovery.cpp
test_filament_bitmap_utils.cpp
test_device_progress.cpp
test_device_manager_integration.cpp
+128
View File
@@ -0,0 +1,128 @@
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <memory>
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/Utils/IPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
using namespace Slic3r;
TEST_CASE("Moonraker permits an empty access code", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
machine.set_access_code("", false);
REQUIRE(machine.has_access_right());
}
TEST_CASE("Moonraker logout revokes access until explicitly rebound", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
const std::string code = GENERATE("", "configured-key");
machine.set_access_code(code, false);
REQUIRE(machine.has_access_right());
machine.revoke_access();
REQUIRE(machine.get_access_code().empty());
REQUIRE_FALSE(machine.has_access_right());
machine.set_access_code(code, false);
REQUIRE(machine.has_access_right());
REQUIRE(machine.get_access_code() == code);
}
TEST_CASE("Revoked access prevents calls to the printer agent", "[DeviceAccess]")
{
class RecordingAgent : public MoonrakerPrinterAgent
{
public:
RecordingAgent() : MoonrakerPrinterAgent("") {}
int connections = 0;
int connect_printer(const PrinterConnectionParams&) override
{
++connections;
return 0;
}
int disconnect_printer() override { return 0; }
};
auto agent = std::make_shared<RecordingAgent>();
NetworkAgent network(nullptr, agent);
MachineObject machine(nullptr, &network, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = GENERATE(BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
machine.set_access_code("configured-key", false);
REQUIRE(machine.connect() == 0);
REQUIRE(agent->connections == 1);
machine.revoke_access();
REQUIRE(machine.connect() == -1);
REQUIRE(agent->connections == 1);
machine.set_access_code("configured-key", false);
REQUIRE(machine.connect() == 0);
REQUIRE(agent->connections == 2);
}
TEST_CASE("Other printer agents require an access code", "[DeviceAccess]")
{
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = GENERATE(
BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, "qidi", "snapmaker", "crealityprint", "plugin", "unknown");
machine.set_access_code("", false);
REQUIRE_FALSE(machine.has_access_right());
machine.set_access_code("88888888", false);
REQUIRE(machine.has_access_right());
}
TEST_CASE("Rebinding Moonraker with an empty key saves the machine", "[DeviceAccess]")
{
AppConfig config;
DeviceManager manager(nullptr, false, &config);
MachineObject machine(&manager, nullptr, "test", "test_dev", "127.0.0.1");
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
machine.dev_connection_type = "lan";
machine.revoke_access();
machine.set_access_code("");
const auto& machines = config.get_local_machines();
REQUIRE(machines.count("test_dev") == 1);
REQUIRE(machines.at("test_dev").printer_agent_id == MOONRAKER_PRINTER_AGENT_ID);
REQUIRE(machines.at("test_dev").access_code.empty());
}
TEST_CASE("Moonraker discovery leaves another agent's saved machine alone", "[DeviceAccess]")
{
AppConfig config;
NetworkAgent network(nullptr, std::make_shared<MoonrakerPrinterAgent>(""));
DeviceManager manager(&network, false, &config);
const std::string owner = GENERATE(as<std::string>{}, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
BBLocalMachine saved;
saved.dev_id = "192.0.2.10:7125";
saved.dev_ip = saved.dev_id;
saved.printer_agent_id = owner;
config.update_local_machine(saved);
manager.on_machine_alive(R"({"dev_name":"Klipper","dev_id":"192.0.2.10:7125","dev_ip":"192.0.2.10:7125",
"dev_type":"","dev_signal":"0","connect_type":"lan","bind_state":"free"})");
REQUIRE((manager.get_local_machine(saved.dev_id) != nullptr) == (owner == MOONRAKER_PRINTER_AGENT_ID));
}
@@ -0,0 +1,51 @@
#include <catch2/catch_test_macros.hpp>
#include <string>
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
using namespace Slic3r;
namespace {
class DiscoveryAgent : public MoonrakerPrinterAgent
{
public:
using MoonrakerPrinterAgent::MoonrakerPrinterAgent;
using MoonrakerPrinterAgent::device_info;
mutable unsigned fetch_count = 0;
protected:
bool fetch_device_info(const std::string&, const std::string&, MoonrakerDeviceInfo& info, std::string&) const override
{
++fetch_count;
info.dev_name = "Test printer";
return true;
}
};
} // namespace
TEST_CASE("Moonraker discovery waits for a complete device identity", "[MoonrakerDiscovery]")
{
DiscoveryAgent agent("");
unsigned announcements = 0;
nlohmann::json announcement;
agent.set_on_ssdp_msg_fn([&](std::string payload) {
++announcements;
announcement = nlohmann::json::parse(payload);
});
agent.start_discovery(true, false);
REQUIRE(announcements == 0);
REQUIRE(agent.fetch_count == 0);
agent.device_info.dev_id = "127.0.0.1:7125";
agent.device_info.dev_ip = agent.device_info.dev_id;
agent.device_info.base_url = "http://127.0.0.1:7125";
agent.start_discovery(true, false);
REQUIRE(announcements == 1);
REQUIRE(agent.fetch_count == 1);
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
REQUIRE(announcement.at("dev_name") == "Test printer");
}
@@ -0,0 +1,111 @@
#include <catch2/catch_test_macros.hpp>
#include <utility>
#include <memory>
#include <vector>
#include <functional>
#include "slic3r/Utils/bambu_networking.hpp"
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
using namespace Slic3r;
namespace {
class DiscoverySource : public MoonrakerPrinterAgent
{
public:
DiscoverySource() : MoonrakerPrinterAgent("") {}
OnMsgArrivedFn discovery_callback;
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
discovery_callback = std::move(fn);
return 0;
}
};
} // namespace
TEST_CASE("Queued discovery cannot cross printer agent sessions", "[NetworkDiscovery]")
{
auto source = std::make_shared<DiscoverySource>();
NetworkAgent network(nullptr, source);
std::vector<std::function<void()>> pending;
std::vector<std::string> delivered;
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
auto outgoing_callback = source->discovery_callback;
outgoing_callback("unsaved-device");
outgoing_callback("saved-moonraker-device");
REQUIRE(delivered.empty());
REQUIRE(pending.size() == 2);
bool expect_active = true;
SECTION("Switch to another agent")
{
network.set_printer_agent(std::make_shared<DiscoverySource>());
}
SECTION("Clear the live agent")
{
network.set_printer_agent(nullptr);
expect_active = false;
}
SECTION("Switch away and back to the cached agent")
{
network.set_printer_agent(std::make_shared<DiscoverySource>());
network.set_printer_agent(source);
}
SECTION("Unregister discovery")
{
network.set_on_ssdp_msg_fn(nullptr);
expect_active = false;
}
SECTION("Replace the discovery listener")
{
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
}
for (auto& fn : pending)
fn();
pending.clear();
REQUIRE(delivered.empty());
outgoing_callback("late-outgoing-device");
REQUIRE(pending.empty());
REQUIRE(delivered.empty());
if (expect_active) {
auto active = std::dynamic_pointer_cast<DiscoverySource>(network.get_printer_agent());
REQUIRE(active);
REQUIRE(active->discovery_callback);
active->discovery_callback("active-device");
REQUIRE(delivered.empty());
REQUIRE(pending.size() == 1);
pending.front()();
REQUIRE(delivered == std::vector<std::string>{"active-device"});
}
}
TEST_CASE("Discovery callbacks expire when networking is destroyed", "[NetworkDiscovery]")
{
auto source = std::make_shared<DiscoverySource>();
std::vector<std::function<void()>> pending;
unsigned delivered = 0;
{
NetworkAgent network(nullptr, source);
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
network.set_on_ssdp_msg_fn([&](std::string) { ++delivered; });
source->discovery_callback("queued-device");
}
REQUIRE(pending.size() == 1);
pending.front()();
pending.clear();
source->discovery_callback("late-device");
REQUIRE(pending.empty());
REQUIRE(delivered == 0);
}
+1 -1
View File
@@ -7,7 +7,7 @@ set(SLIC3R_APP_KEY "OrcaSlicer")
if(NOT DEFINED BBL_INTERNAL_TESTING)
set(BBL_INTERNAL_TESTING "0")
endif()
set(SoftFever_VERSION "2.5.0-alpha")
set(SoftFever_VERSION "2.5.0-dev")
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)"
SoftFever_VERSION_MATCH ${SoftFever_VERSION})
set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})