Compare commits

..
Author SHA1 Message Date
Hanif Koh 258bbee999 build_linux.sh: Forward Arguments After -- to the CMake Configure Step
Everything after `--` is appended, unchanged and last, to the cmake
configure line of the deps and OrcaSlicer builds, so a custom compiler,
compile flags or linker can be passed without adding an option to the
script for each one. The arguments are kept as an array, so a value with
spaces such as -DCMAKE_CXX_FLAGS="-O2 -march=native" survives, and they are
forwarded into the -g container. A stray argument without `--` aborts with
a message instead of reaching cmake as a path.

The existing DEPS_EXTRA_BUILD_ARGS and ORCA_EXTRA_BUILD_ARGS hooks are
unchanged and now mentioned in the usage text.
2026-10-14 14:45:30 +08:00
46b3871f8d fix: login crash when network plugin is unavailable (#16438)
* fix: login crash when network plugin is unavailable

* fix: directly use !config_version.empty() for the control block

* Use if/else for the empty plug-in version check

* Fix multi-device task list using a deleted network agent after plug-in reload

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-14 13:19:04 +08:00
Lam Wei Lun 84768da6a1 fix(gui): scroll the main preset list while its submenu is open
The filament list groups presets by vendor: hovering a vendor in the main
popup opens a side popup with that vendor's materials. On GTK the side popup
holds the popup grab, and gtk_main_do_event hands every GDK_SCROLL of the popup
chain to the grab widget. GDK names the event for the window under the cursor,
so the side popup received the wheel events raised over the main list and
consumed them: the main list could not be scrolled while a vendor was hovered.
The problem shows on both X11 and Wayland, because the rerouting happens in
GTK, not in the compositor.

The side popup now inspects the event's GDK window and, when the event belongs
to the main popup, replays it there. The replay goes through
gtk_propagate_event(GetConnectWidget()): wxGTK connects its mouse handlers to
the inner client widget of a popup (a wxPizza child of the outer GtkWindow), so
a signal emitted on the outer GtkWindow reaches no handler at all.

The widget reference in .claude/skills/orca-wxwidgets records the mechanism and
the connect-widget fact.
2026-10-14 12:00:40 +08:00
Javad Shafique bb0d8ee1c4 fix: use default webkitgtk User-Agent instead of MacOS User-Agent for linux builds (#15104)
Use default Linux webview user agent
2026-10-14 11:36:21 +08:00
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
38 changed files with 1058 additions and 254 deletions
@@ -570,6 +570,11 @@ Details [source] (`Widgets/PopupWindow.cpp`).
- On GTK the sub binds an idle handler. While both are shown and the cursor is over main but not over the
sub, it builds a `wxMouseEvent(wxEVT_MOTION)` from `wxGetMousePosition()`, calls `mainDropDown->mouseMove(ev)`
**directly** (nothing is posted), and calls `e.RequestMore()` to keep polling.
- On GTK the sub also receives the wheel events the user raises over main: GDK sends a `wl_pointer.axis` only
to the surface the pointer is over, and GTK then hands that `GDK_SCROLL` to the chain's grab widget, which is
the sub. The sub installs a `GtkWidget::event` handler that replays the event on main with
`gtk_propagate_event(main->GetConnectWidget(), event)` when the event's GDK window belongs to main
(`DropDown::messureSize`).
- **GTK wrapper sizing.** `messureSize` calls `gtk_window_resize` on `m_widget` after `SetSize`, because wxGTK's
`DoSetSize` only uses `gtk_widget_set_size_request` ("gtk_window_resize does not work for GTK_WINDOW_POPUP",
`src/gtk/popupwin.cpp:181-188`).
@@ -607,6 +612,32 @@ Details [source] (`Widgets/PopupWindow.cpp`).
```
Cite: c0ae2bda99, b9ff15054f (`DropDown::mouseMove`, `DropDown::messureSize`, `DropDown::ShouldDismissOnTopWindowDeactivate`,
`PopupWindow::ShouldDismissOnTopWindowDeactivate`).
- **Rule:** On GTK, route a chained popup's wheel event by the GDK window it names, not by the grab widget and not
by the global cursor position.
**Why:** While the sub holds the grab, `gtk_main_do_event` hands every `GDK_SCROLL` of the chain to the grab
widget (the sub) with its original, un-rebased coordinates [source] (`gtkmain.c:1789-1792`, 1923-1924), while
GDK Wayland sends the axis to the surface under the pointer only (`pointer_handle_axis` → `create_scroll_event`,
`gdkdevice-wayland.c:1929, 1527`). The sub therefore sees and consumes wheel events raised over main, so the
main list never scrolls, on X11 and Wayland alike. A hit test against `wxGetMousePosition()` cannot replace the
window test: global coordinates are unreliable on Wayland (`mouse-keyboard-focus.md`). `DropDown` connects a
`GtkWidget::event` handler on the sub's `GtkWindow` (its `m_widget`), which is emitted before wx's
`::scroll-event` handler on the same widget and suppresses it when it returns TRUE (`gtkwidget.c:7696`), then
delivers the event with `gtk_propagate_event(main->GetConnectWidget(), event)`. Returning TRUE there also stops
the parent-chain propagation.
```cpp
// Wrong: emitted on the popup's outer GtkWindow, where wx has no handlers at all
g_signal_emit_by_name(main->m_widget, "scroll-event", event, &handled);
// Right (wxGTK): replay GTK's own delivery on the widget wx listens to
gtk_propagate_event(main->GetConnectWidget(), event);
```
**Where wx listens [source].** `wxPopupWindow::Create` builds a popup from two widgets: `m_widget` is the outer
`GtkWindow` and `m_wxwindow` is a `wxPizza` child added to it. `wxWindowGTK::GetConnectWidget()` is
`m_wxwindow ? m_wxwindow : m_widget`, and `ConnectWidget()` — which binds `button_press_event`, `motion_notify_event`
and `scroll_event` — is called on that widget (`src/gtk/window.cpp` `Create`, `GetConnectWidget`). A signal emitted
on the outer `GtkWindow` therefore reaches no wx handler; only events GTK delivers to the inner widget, or to a
descendant of the grab widget, are seen by wx. `GTKGetConnectWindow()`/`GTKGetMainWindow()` are the matching
GdkWindows.
Cite: `DropDown::messureSize`, `DropDown::mouseWheelMoved`.
- **Rule:** When shrinking a dropdown to fit above the display bottom, give up if fewer than two rows would
remain.
**Why:** On Wayland the popup can land where almost no vertical space is left. Clamping to that sliver made an
+22 -3
View File
@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null
function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L]"
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L] [-- CMAKE_ARGS...]"
echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)"
echo " -J N: build up to N dependencies at a time, each still using -j jobs (default: 1)"
@@ -29,11 +29,15 @@ function usage() {
echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)"
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and OrcaSlicer (-s/-t) builds,"
echo " e.g. -DCMAKE_CXX_FLAGS=\"-O2 -march=native\", -DCMAKE_CXX_COMPILER=g++-14, -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold."
echo " They come last on the cmake command line, so they override what -l and -L set."
echo "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
echo "Use './${SCRIPT_NAME} -gF -istrlL' to rebuild the cached runner image first."
echo "Set ORCA_CONTAINER_CLI, ORCA_DOCKER_IMAGE, ORCA_DOCKER_BASE_IMAGE, or ORCA_DOCKER_CMAKE_VERSION to override the container runtime, cached image tag, base image, or CMake version."
echo "DEPS_EXTRA_BUILD_ARGS and ORCA_EXTRA_BUILD_ARGS add whitespace-separated cmake configure arguments to the deps and OrcaSlicer builds; prefer '--' for arguments that contain spaces."
}
SLIC3R_PRECOMPILED_HEADERS="ON"
@@ -131,6 +135,20 @@ while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
esac
done
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
if [[ ${OPTIND} -le $# ]] ; then
separator_index=$((OPTIND-1))
if [[ "${!separator_index}" != "--" ]] ; then
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
exit 1
fi
fi
shift $((OPTIND-1))
EXTRA_CMAKE_ARGS=("$@")
if [[ ${#EXTRA_CMAKE_ARGS[@]} -gt 0 ]] ; then
FORWARDED_ARGS+=("--" "${EXTRA_CMAKE_ARGS[@]}")
fi
if [ ${OPTIND} -eq 1 ] ; then
usage
exit 1
@@ -547,7 +565,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}" "${EXTRA_CMAKE_ARGS[@]}"
# The top-level build runs one dependency at a time by default, which keeps the console
# output readable and lets that dependency's own build use all of CMAKE_BUILD_PARALLEL_LEVEL.
# -J raises the top level instead, and -j still applies in full to every dependency, so the
@@ -593,7 +611,8 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \
"${BUILD_ARGS[@]}"
"${BUILD_ARGS[@]}" \
"${EXTRA_CMAKE_ARGS[@]}"
echo "done"
if [[ -n "${BUILD_ORCA}" ]]; then
echo "Building OrcaSlicer ..."
+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);
+80 -86
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);
@@ -3925,81 +3916,74 @@ bool GUI_App::on_init_network(bool try_backup)
if (config_version.empty()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download";
m_networking_need_update = true;
} else {
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
if (!m_device_manager)
m_device_manager = new Slic3r::DeviceManager();
if (!m_user_manager)
m_user_manager = new Slic3r::UserManager();
return false;
}
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
}
}
}
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
}
@@ -4027,6 +4011,11 @@ bool GUI_App::on_init_network(bool try_backup)
else
m_user_manager->set_agent(m_agent);
// A hot reload deletes the agent the task manager was built with. It lives until restart, so repoint it
// even if multi-device has since been turned off.
if (m_task_manager)
m_task_manager->set_agent(m_agent);
if (this->is_enable_multi_machine()) {
if (!m_task_manager) {
m_task_manager = new Slic3r::TaskManager(m_agent);
@@ -4231,8 +4220,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 +4265,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 +4294,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 +4303,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 +10133,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);
+12 -5
View File
@@ -171,6 +171,11 @@ TaskManager::TaskManager(NetworkAgent* agent)
;
}
void TaskManager::set_agent(NetworkAgent* agent)
{
m_agent = agent;
}
int TaskManager::start_print(const std::vector<PrintParams>& params, TaskSettings* settings)
{
@@ -240,17 +245,18 @@ int TaskManager::schedule(TaskStateInfo* task)
fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx);
int result = -1;
if (!m_agent) {
NetworkAgent* agent = m_agent;
if (!agent) {
BOOST_LOG_TRIVIAL(trace) << "task_manager: NetworkAgent is nullptr";
}
else {
assert(m_agent);
assert(agent);
try {
// DEBUG FOR TEST
#if 0
result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
#else
result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
result = agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn);
#endif
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw: " << ex.what();
@@ -363,12 +369,13 @@ std::map<int, TaskStateInfo*> TaskManager::get_local_task_list()
std::map<std::string, TaskStateInfo> TaskManager::get_task_list(int curr_page, int page_count, int& total)
{
std::map<std::string, TaskStateInfo> out;
if (m_agent) {
NetworkAgent* agent = m_agent;
if (agent) {
TaskQueryParams task_query_params;
task_query_params.limit = page_count;
task_query_params.offset = curr_page * page_count;
std::string task_info;
int result = m_agent->get_user_tasks(task_query_params, &task_info);
int result = agent->get_user_tasks(task_query_params, &task_info);
BOOST_LOG_TRIVIAL(trace) << "task_manager: get_task_list task_info=" << task_info;
if (result == 0) {
try {
+5 -1
View File
@@ -16,6 +16,7 @@
#include <vector>
#include <map>
#include <mutex>
#include <atomic>
#include <wx/event.h>
@@ -158,6 +159,8 @@ public:
static int SendingInterval;
TaskManager(NetworkAgent* agent);
void set_agent(NetworkAgent* agent);
int start_print(const std::vector<PrintParams>& params, TaskSettings* settings = nullptr);
static void set_max_send_at_same_time(int count);
@@ -184,7 +187,8 @@ private:
std::vector<boost::thread*> m_sending_thread_list;
std::mutex m_scedule_mutex;
bool m_started { false };
NetworkAgent* m_agent { nullptr };
// Replaced on the main thread whenever networking is re-initialised, read by the sending threads.
std::atomic<NetworkAgent*> m_agent { nullptr };
std::chrono::system_clock::time_point last_sent_timestamp;
};
+30
View File
@@ -19,6 +19,7 @@
#include <wx/stattext.h>
#include <wx/gdicmn.h>
#include <wx/window.h>
#include <wx/utils.h>
#ifdef __WXGTK__
#include <gtk/gtk.h>
@@ -665,6 +666,35 @@ void DropDown::messureSize()
sub->mainDropDown->mouseMove(ev);
e.RequestMore();
});
// Orca: the submenu holds the GTK grab, so GTK gives it the wheel events of the whole
// chain. GDK names the window under the cursor, so re-emit main's events on main.
g_signal_connect(sub->m_widget, "event", G_CALLBACK(+[](GtkWidget *, GdkEvent *event, gpointer data) -> gboolean {
auto *sub = static_cast<DropDown *>(data);
DropDown *main = sub->mainDropDown;
if (event->type != GDK_SCROLL || !main || !main->IsShown() || !sub->IsShown())
return FALSE;
gpointer owner = nullptr;
if (event->any.window)
gdk_window_get_user_data(event->any.window, &owner);
GtkWidget *widget = (owner && GTK_IS_WIDGET(owner)) ? GTK_WIDGET(owner) : nullptr;
bool for_main = widget && (widget == main->m_widget || gtk_widget_is_ancestor(widget, main->m_widget));
bool for_sub = widget && (widget == sub->m_widget || gtk_widget_is_ancestor(widget, sub->m_widget));
if (!for_main && for_sub) {
// The compositor may keep the pointer focus on the submenu while the cursor is over main.
wxPoint pt((int) event->scroll.x, (int) event->scroll.y);
if (sub->GetClientRect().Contains(pt))
return FALSE;
wxPoint off = sub->GetScreenRect().GetTopLeft() - main->GetScreenRect().GetTopLeft();
event->scroll.x += off.x;
event->scroll.y += off.y;
for_main = true;
}
if (!for_main)
return FALSE;
// wx connects its mouse handlers to GetConnectWidget(), the inner widget of the popup.
gtk_propagate_event(main->GetConnectWidget(), event);
return TRUE;
}), sub);
#endif
subDropDown->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent &e) {
e.SetEventObject(this);
+4
View File
@@ -336,9 +336,11 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
s_schemes_registered = true;
}
webView->Create(parent, wxID_ANY, url2, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
#ifndef __linux__
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), Slic3r::GUI::wxGetApp().dark_mode() ? "dark" : "light", language_code.mb_str()));
#endif
#endif
#ifdef __WXMAC__
WKWebView * wkWebView = (WKWebView *) webView->GetNativeBackend();
Slic3r::GUI::WKWebView_setTransparentBackground(wkWebView);
@@ -470,8 +472,10 @@ void WebView::RecreateAll()
wxString language_code = Slic3r::GUI::wxGetApp().current_language_code().BeforeFirst('_');
language_code = language_code.ToStdString();
for (auto webView : g_webviews) {
#ifndef __linux__
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), dark ? "dark" : "light", language_code.mb_str()));
#endif
// A host-themed WebViewHostDialog re-themes in place (no reload). If it handles
// the event, skip the reload; legacy pages fall through and reload as before
// (their own dark.css swap re-themes them on reload).
-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})