Commit Graph
90 Commits
Author SHA1 Message Date
HanifKoh f8cf34c55b Reset the Fan Mover Instead of Releasing It (#16386)
Clearing per-export state called unique_ptr::release(), which gives up ownership without deleting. Every export uses a new GCode, so the pointer is still empty there today and nothing leaks; reset() states the intent and would free the object if a GCode were ever reused.
2026-10-12 13:42:19 +08:00
HanifKohandraistlin7447 95d3e8eafc Queue Pull Request macOS Builds for a Free Runner (#16343)
* ci: wait for the base branch's macOS deps instead of rebuilding them

* Hold Pull Request macOS Builds Until a Runner Is Free

A pull request's macOS build now waits in one repo-wide line, from a
Linux job, until the macOS runners every active Build all run holds or
still needs leave two free. A run is let in whole: its first arch takes
both runners and its second goes straight through, since letting arches
in one at a time deadlocks with every runner held by a run waiting for
its other arch. Push, nightly and manual runs reserve two runners until
their macOS work is done, and a pull request run keeps its two until its
universal build and tests finish, so jobs that never pass the line are
accounted for. Queued jobs count as busy. API errors and a four-hour
limit let the build in rather than block it.

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Count Pending Runs That Hold macOS Runners

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Show the macOS Admission Checks Live in the Job Log

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Let a Labelled Pull Request Go Ahead of the macOS Line

A pull request labelled macos-priority when its run starts waits in its
own line under the same rule, and the normal line counts every priority
run still waiting as holding two runners, so the next free runners go
to it. It still waits for runners in use and for main's reserve.

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Let Each macOS Arch In on Its Own Without Holding an Idle Runner

Letting a pull request run in whole deadlocks when its second arch
reaches the line behind another pull request: the run holds two
runners, the pull request in front waits for them, and the second arch
never gets to check. Each arch now waits on its own. A run with one
arch let in holds a runner only until that arch's app build is done,
and a run with both holds two until its universal build and tests
finish, so whoever is first in line always gets in eventually.

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Stop Holding Runners for a Pull Request's Universal Build and Tests

Holding two runners from a pull request's second arch until its
universal build and tests finished kept one idle through the slower
arch's build. Replaying the week of 10-03 against the real queue, that
idle capacity pushed the median pull request from under three hours to
over sixteen. Those jobs take minutes, so they now queue like any other
job, and a pull request holds a runner only for an arch that is still
building. Each arch then always needs exactly one runner.

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Count Runs That Move Between Status Lists

A run is listed as pending while any of its jobs waits in a concurrency group, so it moves to in_progress the moment its last gate leaves the line. A check made in those seconds read the in_progress list before the move and the pending list after it, missed the run, and let in a second arch the limit had no room for. Runs created in the last day are now listed by creation time as well, and the run doing the check is read directly.

Co-authored-by: raistlin7447 <kris.austin@gmail.com>

* Log API Calls and the Rate Limit Left on Each macOS Admission Check

The calls per check grow with the number of active runs, and the token's
hourly limit is shared by the whole repository, so each check now logs how
many calls it made and what the last response says is left.

* Give the macOS Admission Jobs Fixed Names

A skipped job is listed under its unevaluated name, so every Linux and
Windows leg showed the label expression. Split the waiting job into a normal
and a priority job with fixed names, sharing their steps through a YAML
anchor.

---------

Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-10-12 01:15:47 +08:00
HanifKoh 8145b0792f Free macOS Disk Space Only When the Build Might Not Fit (#16364) 2026-10-11 23:55:23 +08:00
HanifKoh 7369674d8f Add #pragma once to Headers Without an Include Guard (#16362) 2026-10-11 17:26:05 +08:00
HanifKoh 70da7e89bb Release the Slice Button's Mouse Capture When a Press Loses Its Release (#16358) 2026-10-11 15:49:04 +08:00
HanifKoh a671bf605d Dismiss the Slice-Plate Popup on Every Path Into the Filament Grouping Dialog (#16359)
#16284 closed the Slice-plate hover popup only when the Slice button opened
the filament grouping dialog. The dialog also opens from the plate's filament
grouping icon, the preview legend, slicing notification links and the send
dialog, and a popup still shown on those paths keeps the mouse capture on
macOS and starves the modal dialog of clicks. Dismiss it in
FilamentMapDialog::ShowModal() instead, so every path is covered.

The popup could also stay up on its own on macOS: an enter event stopped its
close timer, and when the timer fired with the cursor inside it returned
without re-arming, so without a later leave event the popup never closed. On
macOS the timer now keeps running while the popup is shown and closes it once
the cursor is outside. The popup also stops and frees its timer when destroyed.
2026-10-11 15:48:49 +08:00
HanifKoh 2df07b7ed8 Verify the TLS Certificate When Downloading a Cloud Plugin (#16253) 2026-10-11 14:25:50 +08:00
HanifKoh d715bb760c Let Plugin Windows Be Maximized on X11 (#16335)
wxGTK types every wxDialog as a dialog window, and Mutter offers no
maximize for anything but a normal window, so the maximize button and
shortcut did nothing on GNOME under X11. Modeless plugin windows are now
normal windows, kept off the taskbar as before. Modal ones stay dialogs
so the window manager keeps routing focus from the main window to them.
2026-10-11 04:40:56 +08:00
HanifKoh de5f2cb3b9 Expose App, GL, Printer and Project Facts to Plugins (#16324)
* Expose App, GL, Printer and Project Facts to Plugins

Plugins that help users file bug reports could only guess these from log lines: the app config is
deny-listed, and the project path, a project export and the device list had no API.

orca.host gains app_info(), gl_info() and selected_printer(), and Plater gains project_path() and
export_3mf_copy(path). The copy export leaves the project's file name, saved state and model
unchanged, does not write the signed-in account as the designer, and raises the audit event of
open(path, "w") so the plugin gets the same permissions as for writing the file itself.

* Remove app_language in Favour of app_info

app_info() returns the same language. app_language() was added after the last release, so no released build has it.
2026-10-11 04:35:37 +08:00
HanifKoh 65978fc87a Cap Recursion Depth in the Plugin JSON Converters (#16255)
* Cap Recursion Depth in the Plugin JSON Converters

* Add Missing Includes to the Plugin JSON Depth Test
2026-10-09 11:39:46 +08:00
HanifKoh 9a9285a894 Log Instead of Showing Info and Substitution Dialogs When Opening a BambuStudio Project (#16153)
Every BambuStudio project opened with a "BambuStudio Project" info dialog
(or, from BambuStudio 2.8.2, one saying the file is newer than the
compatible version and to update the software), followed by the
configuration-substitution dialogs for the project and its embedded
presets. None of them asks anything and all of them fire for every
BambuStudio file.

For BambuStudio projects (untagged files newer than 2.3.2, the existing
test) log the version with the unrecognized settings, and each replaced
value, instead. The geometry-only, invalid-values and G-code safety
dialogs stay, and other 3MFs are unchanged.
2026-10-09 11:33:53 +08:00
HanifKoh ae59656fa4 Dismiss the Slice-Plate Popup Before the Filament Grouping Dialog Opens (#16284)
The Slice-plate hover popup (FilamentGroupPopup, a wxPopupTransientWindow)
takes the mouse capture while it is shown, and on macOS its OnIdle handler
reacquires that capture whenever the cursor sits outside the popup. If the
popup is still shown when the modal filament grouping dialog opens, wx routes
every dialog mouse event to the now-hidden popup, because WX_filterSendEvent
short-circuits to the capture window while GetCapture() is non-null. The
dialog's filament blocks never receive a mouse-down, so they can't be dragged
and the whole app looks frozen even though its modal loop is healthy and the
keyboard still works.

Dismiss the popup synchronously before the dialog opens: Dismiss() hides it
and releases the capture, and hiding it stops OnIdle from reacquiring. This is
a no-op where the popup is never shown (Linux, where the hover popup is
disabled, and any non-dual-nozzle printer).
2026-10-09 11:33:24 +08:00
HanifKoh 18b70844d1 Defer Plugin Dock Panes Until the Plater Is Shown (#16257)
A plugin enabled at startup loads before the main frame exists, so a
dock panel it opens from on_load was dropped by the one-shot CallAfter
that found no plater. Opened a moment later, before the frame was laid
out, the pane was sized against the unsized frame and track_docked_size
kept that width. Poll until the plater is shown on screen, then build
the pane; release the reserved id instead when the app is closing.
2026-10-08 14:18:51 +08:00
HanifKoh c8f2498681 Keep GLCanvas3D Building With SLIC3R_CAD Off (#16256)
The bed-axes toggle added for the Design tab's reference planes reads
m_design_sketch_tool, which only exists under SLIC3R_CAD. Compute the
flag once and read the sketch tool inside the same guard as its other
uses.
2026-10-08 14:02:34 +08:00
HanifKoh 30902561c0 Stop Exporting Names Through Usings in the Remaining Headers (#16250)
The last headers with a using or namespace alias at namespace scope:

- TCPConsole.hpp imported boost::asio::ip::tcp into Slic3r::Utils for
  two member declarations. The alias is now a private member of the
  class.
- WebSocketClient.hpp declared four namespace aliases and a tcp alias
  at global scope, each used only by the header. The names are spelled
  out.
- Repair.hpp aliased CGAL::Polygon_mesh_processing as PMP in
  Slic3r::tex2color. The three functions that use it declare the alias
  themselves.
- PreciseSeam.hpp, Thumbnails.hpp and MarchingSquares.hpp used a
  using-declaration or directive for one or two spots each; those spots
  are qualified. Thumbnails.hpp's "PNG"sv default argument becomes
  "PNG", which converts to the std::string_view parameter the same way.
- tests/sla_print/sla_test_utils.hpp had "using namespace Slic3r;" and
  tests/filament_group/fg_test_serialization.hpp "using json =
  nlohmann::json;" at global scope. The headers qualify their own names;
  the two SLA test sources get the directive themselves.

Also removed: twelve type aliases in headers that nothing references
(ConflictObjName, CircleSqf, CircleSqd, TRawBuffer, DistanceFunction,
SamePair, ExtruderNozzleInfos, Vec2dEvent, Vec2dsEvent, Vec3dEvent,
t_option, t_optgroups, Plater::fs_path) and a duplicate
fn_ft_job_msg_destroy alias in FileTransferUtils.hpp.
2026-10-08 13:59:12 +08:00
HanifKoh 3fc515f48d Prompt for Permission When a Plugin Calls os.exec (#16254)
* Prompt for Permission When a Plugin Calls os.exec

* Add a ProcessReplace Audit Category for os.exec
2026-10-08 13:52:09 +08:00
HanifKoh 6cd5feed79 Remove Duplicate Includes and the Dead GCodeSender Sources (#16249)
46 files include the same header twice at file scope, outside any #if,
66 times in all:
Model.cpp included Model.hpp twice, Utils.hpp <algorithm> and
<string_view> twice, seven GUI headers <wx/dataview.h> and
<wx/artprov.h> twice. The second include of each is removed.

GCodeSender.cpp and GCodeSender.hpp have been commented out of
libslic3r/CMakeLists.txt since 2022 and their only two includes are
commented out as well. Both files go, with the commented lines, and
the CMake entry for SLA/SupportTreeIGL.cpp, a file that no longer
exists.
2026-10-08 13:51:50 +08:00
HanifKoh 09530ef7c4 Size a Project's Mixed-Colour Metadata to the Filaments in the CLI (#16247)
The mixed-colour metadata options are parallel per-slot arrays in the project
config. A project saved before they were sized per slot stores a single value
for the gradient ones, and one saved before they existed stores none. The GUI
sizes all seven to the filament count when it opens a project; the CLI kept
the stored arrays and exported one-element defaults for absent ones, so a
project it exported carried one-element arrays where the GUI writes one entry
per filament. Slicing is unaffected, every reader treats a missing entry as
not mixed / no gradient, but the GUI-vs-CLI comparison reported the four
gradient keys on every mixed-filament project.

The resize helper moves from PresetBundle.cpp, where it was file-local, to
PrintConfig.cpp next to set_filament_dev_options(). It creates an option the
config lacks before sizing it, a no-op for the bundle's project config where
all seven always exist. The CLI calls it with its filament count once the
project and loaded filaments are merged, after the check that every mixed
slot has a filament of its own.
2026-10-08 13:40:36 +08:00
HanifKoh f9ddb23804 Keep Each Filament's Device Drying Values When the CLI Merges Filaments (#16157)
The device drying options hold several values per filament, as many as
the filament preset gives, and a project stores them as the filaments'
values one after another. The CLI filament merge wrote them like an
option with one value per filament, putting each preset's first value at
the filament's own index, so a project with three filaments whose preset
gives "1", "0" was exported with 1;1;1;0;1;0 where the GUI writes
1;0;1;0;1;0.

The merge now leaves these options out of the per-filament pass and
rebuilds them afterwards from every filament's values in slot order.
Without a fixed number of values per filament one slot cannot be
replaced in place, so the stored values are kept when any slot has no
config to rebuild from.
2026-10-07 15:54:53 +08:00
HanifKoh ff8aebe76f Allow Unsigned Executable Memory in the macOS Entitlements (#16203)
The Bambu network plug-in's code protector rewrites one page of its own signed __TEXT after loading. The hardened runtime tolerates that until the page is evicted; the next read of it then kills OrcaSlicer with CODESIGNING Invalid Page. Bambu Studio signs with allow-unsigned-executable-memory for this reason; with it added, the same build survives critical memory pressure that killed it in 30 s without.
2026-10-07 15:52:18 +08:00
HanifKoh a56d1bf33e Keep a Project's Changed Values on Extruder Variants It Doesn't List (#16107)
A project's listed settings are carried onto its base preset by update_non_diff_values_to_base_config, which matched variants by exact name and id. A variant the base gained after the project was saved got the base's value, while the same value in a user preset now falls back to the preset's first variant of that extruder. So an old project opened with its printer preset already modified, and saving it wrote the base's values into the 3MF.

The function now maps variants with map_variant_indices, as update_diff_values_to_child_config does: a base variant the project does not list takes the project's first variant of the same extruder. The variant lists themselves stay the base's, so a fallback never writes one variant's name over another's.
2026-10-07 15:51:49 +08:00
HanifKoh 662a8e340d Support the 02.08.04 Bambu Network Plug-in Series (#16202)
The 02.08.02 series appended queue_plate_id to PrintParams and nothing
after it changed the ABI OrcaSlicer calls, so adding the field brings
the current layout up to 02.08.04. Make 02.08.04 the latest series and
drop 02.08.01 from the whitelist: its PrintParams no longer matches, and
its malformed bind table is refused by dyld on macOS 27, so it cannot
load there. A stored 02.08.01 falls back to the latest series through
the existing unsupported-version path.
2026-10-07 15:51:21 +08:00
HanifKoh 6639a32b0c Remove using namespace std from json_diff.hpp (#16222)
The directive sat at global scope in a header that DeviceManager.hpp
includes, so most of the GUI compiled with all of std in the global
namespace. 42 files had come to rely on it, mostly for string, vector
and unordered_map, four of them for the ""sv and ""ms literals.

Those sites are qualified. GCodeViewer.cpp spelled the type as
std::vector<::string>, which only resolved through the directive. The
files that use the ""sv and ""ms literals get a file-scope
"using namespace std::string_view_literals;" or
"using namespace std::chrono_literals;", as other sources already do.
2026-10-07 14:58:49 +08:00
HanifKoh 559ad3e2b7 Flag Global Usings in Headers with clang-tidy (#16225)
A using-directive or using-declaration in the global namespace of a
header reaches every file that includes it, and a using-declaration also
makes the include checker treat that header as the one to include for
the name. google-global-names-in-headers reports both, on changed lines
like the existing check, so headers that still have one are not held to
it until the line is touched.

The check does not see a using inside a namespace.

clang_tidy_diff.py's closing message assumed every finding was a missing
include; it now says other findings need a manual fix.
2026-10-07 14:23:11 +08:00
HanifKoh de6b0b9f2d Remove Header Usings and Aliases Nothing Depends On (#16224)
- ClipperUtils.hpp imported jtMiter, jtRound and jtSquare into the
  global namespace for every includer. No code names them there.
- BBLStatusBar.hpp, BBLStatusBarBind.hpp, BBLStatusBarPrint.hpp,
  BBLStatusBarSend.hpp and ProgressStatusBar.hpp re-exported their class
  into Slic3r::GUI. Nothing refers to the class through that namespace.
- Jobs/SendJob.hpp, Jobs/BindJob.hpp, Jobs/UpgradeNetworkJob.hpp and
  AuxiliaryDataViewModel.hpp declared "namespace fs = boost::filesystem;"
  at global scope without using it.
2026-10-07 14:23:00 +08:00
HanifKoh f02292f163 Stop Exporting Names Through Usings in GUI and Voronoi Headers (#16223)
Each of these headers put a using or namespace alias at global or
namespace scope, which every includer inherited:

- BBLTopbar.hpp: "using namespace Slic3r::GUI;" at global scope, reached
  through MainFrame.hpp. Seven source files used GUI names unqualified
  outside the namespace because of it, one of them as "::RadioBox".
- IMSlider.hpp and TickCode.hpp: "using namespace CustomGCode;" inside
  Slic3r.
- ProjectTask.hpp, Jobs/PrintJob.hpp and ConfigWizard_private.hpp:
  "namespace fs = boost::filesystem;". PresetBundle.cpp and GUI_App.cpp
  had no alias of their own.
- VoronoiUtils.hpp: "using VD = Slic3r::Geometry::VoronoiDiagram;" at
  global scope.

The headers now spell the names out. Source files that used them get
the qualifier, or a using of their own where there are many uses.
2026-10-07 14:22:45 +08:00
HanifKoh 23546e71ce Remove Unused Usings and the Includes Kept Only for Them (#16220)
151 using-directives, using-declarations, type aliases and namespace
aliases in source and test files that nothing refers to: the name is
never used, it duplicates a using already in scope, or the code sits
inside the namespace it names. Each one was removed on its own and the
file still compiled, both as it is and with every header-level using
taken away, so none of them was only redundant because a header leaks
the same name.

With the using gone, 28 #include lines and one forward declaration had
no other reference left in their file (boost/optional.hpp without any
optional, property_tree headers without any ptree) and go with it.

No header is touched.
2026-10-07 14:22:32 +08:00
HanifKoh 73d32d4791 Stop Leaking json Through Headers and Drop Includes Kept Only for the Name (#16221)
* Stop Leaking json Through Headers and Drop Includes Kept Only for the Name

AppConfig.hpp, DeviceManager.hpp and UserManager.hpp carried a global
"using namespace nlohmann;", json_diff.hpp a global "using json =
nlohmann::json;" and PrinterFileSystem.h a global "using nlohmann::json;".
Every file that included one of them, directly or not, could write a
bare json, and 63 did without declaring it.

The last two also made the include checker treat json_diff.hpp and
PrinterFileSystem.h as the headers that provide json, so they were
included from files that use nothing else from them: 57 of the 59
includers of json_diff.hpp never name json_diff.

The five statements are removed. Headers that use the type now spell
nlohmann::json, source files declare their own "using json =
nlohmann::json;", and the includes that only supplied the name are
dropped or replaced by <nlohmann/json.hpp>.

Eight files reached json_diff.hpp only through an include that is now
gone and with it lost that header's "using namespace std;". The std
names they used unqualified are qualified.

* Declare json in OrcaSlicer.cpp on Every Platform

OrcaSlicer.cpp had its "using namespace nlohmann;" and the json include
inside the Linux-only include block, so on Windows and macOS it took
json from AppConfig.hpp's global directive, which is gone. The include
and a "using json = nlohmann::json;" now sit outside the block.
2026-10-07 14:22:16 +08:00
HanifKoh d1afb1fed6 Use a System clang-tidy When Available and Make --fix Converge in One Pass (#16199)
* Use a System clang-tidy When Available and Make --fix Converge in One Pass

scripts/run_clang_tidy.sh only looked at CLANG_TIDY and the venv it creates,
so a clang-tidy already on the system was never used. It is now the first
choice: the pinned version outright, another version after a prompt that
says results may differ slightly from CI, which -y and an existing pinned
venv skip.

Two problems in clang_tidy_diff.py made --fix need several runs and still
leave the plain check failing:

- A deleted #include orphans uses on unchanged lines. The plain check runs
  such a file whole and reports them, but --fix kept the line filter to the
  changed lines, so they were never fixed. Fix mode now runs the file whole
  first and then fixes exactly the changed lines plus the lines that run
  found wanting, so unrelated lines are still never rewritten.

- clang-tidy exits non-zero for the findings it just fixed, so every fixed
  file was reported as failed and the user ran --fix again to see what was
  left. A file --fix changed is now checked again and the fixed files are
  listed separately from what --fix could not add.

CI runs the script without --fix and is unchanged.

* Keep the a/ b/ Diff Prefixes Whatever the User's Git Config Says

parse_diff recognises a changed file by its +++ b/ header. With
diff.noprefix or diff.mnemonicPrefix set, git prints +++ src/x.cpp or
+++ w/src/x.cpp instead, every file was dropped, and the local check
reported no changed C++ lines. The diff is now asked for the a/ and b/
prefixes outright, which overrides both settings.

* Warn When No Remote Points at OrcaSlicer/OrcaSlicer

Without one, run_clang_tidy.sh compares against origin/main. When origin
is a fork whose main already holds the commits, the check finds nothing
and says so, without hinting at why. The script now names the base it
fell back to and how to point it at the upstream repository.
2026-10-06 17:16:09 +08:00
HanifKoh 92de6f1e5b Restore Thread.hpp in MediaPlayCtrl.cpp for the Windows Non-Release Ping Test (#16155)
The only use of Slic3r::create_thread in this file sits behind
!BBL_RELEASE_TO_PUBLIC && __WINDOWS__, so the include looked unused in
every configuration CI builds. Windows Debug and RelWithDebInfo builds
failed without it.
2026-10-05 18:33:49 +08:00
HanifKoh c8edb29ddc Gate Pull Requests on clang-tidy Missing-Include Checks (#16154)
A Linux job configures without the precompiled header and runs clang-tidy over the C++ lines a pull request changes. The only check for now is misc-include-cleaner for missing includes; .clang-tidy is where further checks get enabled.

scripts/run_clang_tidy.sh (Linux, macOS) and scripts/run_clang_tidy.ps1 (Windows) run the same check locally: the same configure, the clang-tidy version pinned in scripts/clang_tidy_requirements.txt, and the same comparison against OrcaSlicer's main. They offer to install what is missing, or print the command to do it by hand.
2026-10-05 18:27:22 +08:00
HanifKoh 4895bc03b4 Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced (#16099)
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced

Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes.

clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer.

* Remove Unused Project Includes From Files With Platform-Specific Code

A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block.

* Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass

The platform-file pass treated pchheader.hpp as an ordinary header and
emptied it, and left GUI_Preview.hpp and 14 other files relying on
headers they no longer reached directly.

* Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call

* Include Headers That Files Reached Through Ones the Cleanup Removed

* Drop Includes Duplicated by the Cleanup or by Main's Own Additions

* Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
2026-10-05 16:47:17 +08:00
HanifKoh 63d5fa23b6 Remove the OBJ Color Dialog That Texture Import Replaced (#16122)
* Remove the OBJ Color Dialog That Texture Import Replaced

* Remove references to ObjColorDialog
2026-10-05 14:39:10 +08:00
HanifKoh 1869895b0a Load One jQuery 3.6.0 Copy from include on Every Web Page (#16143)
Home, Project and seven setup-guide pages move from jQuery 2.1.1 to 3.6.0, and the four other copies are removed.
2026-10-05 14:32:38 +08:00
HanifKoh 48a8e33ec3 Delete Gizmo, SLA and Utils Files Nothing Builds or Includes (#16101)
GLGizmoSlaSupports, GLGizmoHollow, GLGizmoFaceDetector, GLGizmoText and GLGizmoAdvancedCut were already left out of the build, and GLGizmos.hpp, the only header including some of them, had no includers. VoxelizeCSGMesh.hpp uses types that no longer exist, SLA/bicubic.h does not compile, and Utils/ProfileDescription.hpp is included nowhere. Their CMake and gettext source-list entries go with them.
2026-10-05 14:26:15 +08:00
HanifKoh 2dbfc6bc5b Add Missing Includes to Tests Merged Since the Last Include Sweep (#16146) 2026-10-05 14:19:47 +08:00
HanifKoh 017bf0a2c8 Remove Unused Web Resources: Duplicate Swiper and jQuery, Test Data and Images (#16119)
* Trim the Bundled Swiper Library to the Files the Web Pages Load

* Remove Unused jQuery Copies, Model Test Data and Unreferenced Web Images
2026-10-05 14:16:59 +08:00
HanifKoh c44f2d6324 Remove the Unreachable MakerWorld Publish Dialog (#16121) 2026-10-05 14:12:00 +08:00
HanifKoh 88346efceb Stop Format/STEP.hpp Defining a Global fs Alias (#16102)
Every file that included STEP.hpp, directly or not, got namespace fs = boost::filesystem at global scope, and 29 sources and three headers relied on it without saying so. Headers now spell out boost::filesystem, and each source that uses fs declares the alias itself.
2026-10-04 14:50:30 +08:00
HanifKoh 5a95ba4bd5 Add Missing Includes to Code Merged Since the Include Cleanup (#16106)
* Add Missing Includes to Code Merged Since the Include Cleanup

* Add Missing Includes to Code Merged Since the Previous Sweep
2026-10-04 14:50:15 +08:00
HanifKoh d1a3ef68c5 Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs (#15978)
* Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs

Four CLI paths indexed vectors without checking their size and crashed
with SIGSEGV on malformed input:

- A project inherits_group whose length is not the filament count plus
  the process and printer entries was split by position. It is now
  ignored with a warning, as if the project had none.
- An assemble list object with an empty filaments list passed validation
  and was then read at index 0. It is now rejected as a config error, as
  is a negative filament id.
- --slice N --arrange 1 on a project without plate metadata read the
  missing plate data. It now falls back to the plate's own filaments,
  like the other plate data reads.
- --assemble with no input model built an object with no volumes. It is
  now rejected as invalid parameters.

A tests/cli script covers each case through the binary, since all four
live inline in CLI::run().

* Move the Assemble List Parser into libslic3r

Behaviour-preserving move of the --load-assemble-list JSON parser and
its plate/object structs from the CLI into libslic3r/Format/AssembleList,
so the format can be unit tested. The parser returns its own
AssembleListResult and takes the plate limit as a parameter; CLI::run
maps the result to the same exit codes as before. Every validation rule
and log message is unchanged.

Adds Catch2 coverage of the valid layout and each validation rule.

* Keep the Process and Printer of an inherits_group of the Wrong Length

A project whose inherits_group did not have one entry per filament plus
the process and printer entries was loaded as if it had none. The CLI
then looked for system presets under the names of the user presets,
found none and refused to slice a project that slices on main.

The group is now read as before: the process first, the printer last
and the filaments in between, up to the filament count. A filament
without an entry counts as a system preset. A group with fewer than two
entries is still ignored. The warning stays.
2026-10-04 03:17:40 +08:00
HanifKoh a80c323614 Let the CLI Resolve Presets on Installs That Ship Preset Caches Only (#16047)
Release builds install each vendor as its preset cache alone. The
read-only preset load the CLI uses to resolve an inheriting user preset
passed allow_cache = false to keep caches from being written, which
also stopped them from being read, so every vendor fell back to JSONs
that are not installed and the CLI failed.

The flag now only gates writing: a read-only load reads caches and
writes none. The filament library is also read from its cache whenever
that is all that is installed, so a vendor updated over the air still
resolves against it.
2026-10-04 00:40:38 +08:00
HanifKoh 52ff374870 Refresh a CLI Project's Filament Settings From Their System Presets (#16038)
* Refresh a CLI Project's Filament Settings From Their System Presets

The CLI loads a project's printer and process settings as the GUI does,
taking every key the project does not list as changed from the current
system preset, but it kept the stored filament values. A project saved
before a profile update then sliced with old filament values on the
command line and with the current ones in the GUI.

Every project filament that no loaded filament replaces is now resolved
by its system preset name and fed to the filament merge the up-to-date
path already uses, which keeps the keys listed in
different_settings_to_system and maps per-variant values onto the
preset's variants. This covers a plain run, --uptodate without
--uptodate-filaments, and the slots --load-filaments leaves empty. The
merge tells refreshed entries from loaded ones per entry instead of by
the global loaded-filament count, and the entries are kept in slot
order. A project filament saved under a name the presets have since
split per nozzle is resolved through the name conversion the GUI uses,
which PresetBundle now exposes.

* Check the Project Refresh Test's Result Directly

Shellcheck SC2181: test the checker's exit status in the if instead of
reading $? afterwards.
2026-10-04 00:36:10 +08:00
HanifKoh 8a6377f087 Add Missing Includes Across src/libslic3r (#16068)
* Add Missing Includes Across src/libslic3r

Every libslic3r source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, with libslic3r headers spelled libslic3r/... so they resolve outside the library's private include paths. MultiMaterialSegmentation.hpp, Support/SupportParameters.hpp and Format/STEP.hpp are made self-contained by hand.

* Make the libslic3r Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Left out: I18N.hpp, which errors on purpose when included from GUI code, and VoxelizeCSGMesh.hpp and SLA/bicubic.h, which nothing includes and which no longer compile at all.

* Add the Includes Missing From the Hand-Fixed libslic3r Headers

clang-tidy would not edit these headers while they failed to compile on their own, so the first pass skipped them. With the headers now self-contained, a second pass adds the rest.

* Keep Windows Setup Ahead of the Added libslic3r Includes

Print.cpp and Thread.cpp open with a _WIN32 block that has to come first; without the precompiled header, Print.cpp otherwise reaches windows.h through OCCT with NONLS defined and boost/regex fails. OpenVDBUtils.cpp and SLA/SupportTreeBuilder.cpp had includes inside #ifndef NOMINMAX, which libslic3r defines on Windows, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory.

* Re-Add libslic3r Includes After the Clipper2 2.0.1 Migration

Rebasing onto main took main's version of the files the Clipper2 migration rewrote, so their added includes are restored here, along with includes for main's new code. Clipper2's individual headers are now ignored by clang-tidy: they only build the Z variant through clipper2_z.hpp, which defines USINGZ first, so including clipper.core.h and the like directly broke ClipperZUtils.cpp.
2026-10-03 15:31:11 +08:00
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00
HanifKohandSoftFever 205de9ce63 Keep User Preset Values on Extruder Variants They Don't List (#16046)
* Keep User Preset Values on Extruder Variants They Don't List

A user preset stores the variant list its parent had when it was saved.
When the parent later gains variants, update_diff_values_to_child_config
matched variants by name only and left the new ones at the parent's
value, so the user's settings were silently replaced there, and a
re-save wrote the system values into the user's file.

An unmatched parent variant now takes the child's first variant of the
same extruder, the rule slicing already uses in get_config_index_base.
A child without a variant list covers the parent's first extruder. The
name match also no longer indexes the child's extruder ids when it has
none.

* Share One Variant Column Rule Between Slicing, User Presets and Projects

Three places chose which variant column a value comes from, each with
its own copy of "the same variant and owner, else the owner's first
column": get_config_index_base when slicing, the user preset merge in
update_diff_values_to_child_config, and normalize_filament_values_to_variants
for projects and the CLI.

find_variant_column now holds that rule and map_variant_columns applies
it to a variant list, so a change to how missing variants are filled
reaches all three. Each caller keeps its own copy step. There is no
behaviour change: G-code is identical before and after. The one
relaxation is that get_config_index_base no longer reads past a short
id list when its two lists differ in length, which its assert already
rules out.

* Rename variant column helpers to variant index

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-02 20:13:06 +08:00
HanifKoh 390b7d8e6d Make Painted Multi-Material Slicing Deterministic (#15899)
* Make Painted Multi-Material Slicing Deterministic

Painted (multi-material) models sliced to slightly different G-code on
every run: ±1 µm wall coordinates and reordered islands. Hashing each stage
of the segmentation across runs showed the projected painted lines and the
per-layer Voronoi segmentation were stable; the raw top/bottom projections
from slice_mesh_slabs() were not. Three causes, all thread-order dependent:

- slice_slabs_make_lines() appends each slab's intersection lines from a
  parallel facet loop and never restored a canonical order, so the loop
  start vertices and polygon order from make_slab_loops() depended on
  scheduling. Sort every slab's lines with the same key slice_make_lines()
  already uses.
- segmentation_top_and_bottom_layers() wrote a layer's shell projections
  into neighbouring layers' vectors from the parallel loop, relying on a
  parity double-buffer that assumes TBB ranges are exactly one group wide
  and aligned, which blocked_range does not guarantee; two threads could
  append to the same vector. Each source layer now records its projections
  in its own slot and they are gathered per target layer in source order.
- The painted-line sort in post_process_painted_lines() was not a total
  order: projections of one span from facets of different colours tied on
  every key and the first one won the span. Colour and end points now break
  the tie.

Three multi-threaded runs of each painted fixture now give one G-code;
unpainted output is unchanged.

* Test That Slab Slicing Does Not Depend on the Thread Schedule

Projects a dense, tilted sphere with slice_mesh_slabs() on one thread and
then three times multi-threaded, and requires the polygons to match exactly,
vertex order included. Fails without the canonical line sort, passes with it.
2026-10-02 16:51:06 +08:00
HanifKoh 1a5f91d727 Add Missing Includes Across src/slic3r/GUI (#16048)
* Add Missing Includes Across src/slic3r/GUI

Every GUI source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, plus one hand edit making CalibrationPanel.hpp self-contained.

* Drop the OS-Specific Includes Added Outside Their Platform Guards

GLib, GTK, D-Bus and POSIX headers are only used inside platform #if blocks, which already include them. Added unconditionally at the top of the file they broke the Windows build.

* Add the clang-tidy Configuration That Generated These Includes

Only misc-include-cleaner's missing-include check, with the headers it must never suggest: per-platform, internal and OS-specific ones that would break other platforms or are not meant to be included directly.

* Match Windows Paths in the clang-tidy Ignore List

Header paths use backslashes on Windows, so every / in a pattern is now [/\\]. The Windows SDK headers are ignored alongside the other OS-specific ones, and the list is one pattern per line. Suggested by @raistlin7447 from a Windows clang-cl run.
2026-10-02 14:56:56 +08:00
HanifKoh c023632a7d Lock Config and Preset Files Across Instances and Write Them Atomically (#15861)
* Lock Config and Preset Files Across Instances and Write Them Atomically

Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.

Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock: flock on POSIX, held on the guard's own descriptor so no
other close in the process can drop it, and LockFileEx on Windows. The
outermost guard opens the lock file and closes it on release, so nothing
stays open between saves and a data dir can be removed once nothing is
saving into it; the file itself is kept, since deleting it would let a
third instance lock a fresh file while the second still holds the old
one. It is best effort: when the lock file cannot be opened or locked, or
another instance still holds it after a second, the guard logs once and
lets the write proceed, then leaves the file alone for ten seconds, so a
hung instance never blocks every other one and a holder stuck in a
debugger does not cost a stall per save. The guard sits at the leaf
readers and writers: set_sync_info_and_save() calls save_info() under the
preset collection mutex, so a batch lock around save_user_presets() would
invert the order against the sync thread. The user preset scan reads its
files on worker threads without the guard, since the mutex would
serialise them, and takes it per file in the serial commit step, so a
save never waits for the whole scan. Each read keeps the bytes of the
preset and its .info as they were before parsing; commit compares them
with the disk under the guard and reads a file that changed again, so it
never deletes or writes back over another instance's newer save, nor
installs a .json and .info from two different saves; a preset another
instance removed in the meantime is not installed. Without the guard, in
a cool-down, the scan still loads the presets but leaves their files
alone: an unreadable file stays for the next scan, and a derived
compatible printer is not written back. Read-only scans, which is what
the CLI does, take no lock and create no lock file.

AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is
included because the Windows path restores from the .bak copy. Every
user preset writer and reader holds user.lock: Preset::save(), which
writes no .info when the preset itself could not be written, since an
.info without its preset reads as a cloud deletion request, save_info(),
reload() and remove_files(), each preset the scan commits, the
bundle metadata reads and write, the .info removal after a cloud-confirmed
delete, the orphaned-.info scan on the sync thread, the bundle folder
removal on unsubscribe and the physical printer writers and delete
paths. A bundle import extracts under cache/ into a folder per process
and per import, where no scan reads.

Preset JSON, .info, bundle metadata, physical printer and config files,
and the caches and state files that already used a temporary by hand,
now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp
beside the target and renames it over, so a reader that never waits sees
a complete old or new file. A symlink is followed; a target that is not
a regular file is written in place; and when no temporary can be created
beside an existing target, or the rename itself is refused, by a Windows
reader holding the file open or a mount that cannot replace in one step,
the helper writes in place as before, since losing the save is worse
than a torn read. On POSIX the rename replaces the
target atomically where the old code removed it first and left a window
with no file at all; only a mount that refuses a one-step replace gets
the old remove-then-rename. A crash between temporary and rename leaves
the temporary behind, which no scan reads. Preset::save() returns
whether it wrote the preset, so the scan counts a compatible printer it
could not write back as an error. A failed config write keeps
the config dirty, and the idle handler waits ten seconds before retrying
while an explicit save always tries.

* Run the Cross-Process Lock Test on Every Platform

The test that checks the guard yields to a lock held elsewhere forked a
child to hold it, so it was left out on Windows. The OS lock belongs to
the handle on Windows and to the open file description elsewhere, so a
second handle in the same process is refused like another instance
would be. The test now holds the lock that way and runs everywhere.
2026-10-01 22:18:21 +08:00
HanifKoh 236a8786ef Keep the Orca Cloud Agent Tests Out of the System Keychain (#15980)
The display-name tests call set_user_session(), which persists the session.
The agent they built was in keychain mode, so every run saved a fake
OrcaSlicer/Auth session into the system keychain of whoever ran the tests,
replacing their real Orca Cloud login on any desktop with a working keychain.

Move them next to the other agent tests and build the agent the same way:
encrypted-file mode with a throwaway config directory.
2026-10-01 16:58:52 +08:00
HanifKoh 92d30fbc55 Clamp Ironing Line Spacing to a Usable Minimum (#15949)
An ironing line spacing of 0 reached the fillers from a 3MF, the CLI or
the per-filament override, which has no GUI guard. Concentric ironing
then never finished slicing, because a zero inset never shrinks the
region, and rectilinear ironing was silently dropped. Tiny positive
values produced an unprintable number of lines.

Top surface and support ironing now clamp the spacing to the 0.05 mm
floor the process GUI guard already enforces, so these configurations
iron at that spacing. Spacings at or above the floor, including every
shipped profile, are unchanged. The concentric filler also returns early
on a non-positive step so no other caller can hang it, and the filament
settings page now resets a too-small override the same way the process
page does.
2026-10-01 16:53:16 +08:00
HanifKoh 6842d9c778 Keep the Plugin Tests' Python Packages Out of the Working Directory (#15981)
The plugin test fixtures start the interpreter before the test points
data_dir at its temporary directory, so PythonInterpreter creates
{data_dir}/python/packages and {data_dir}/log with an empty data_dir: a
python/ and log/ folder in whatever directory the tests run from. When that
is the test binary's folder, the next run's embedded-interpreter tests took
the stray python/ as their home and failed to start Python.

Give each fixture that initializes the plugin manager its own temporary
data directory, set up before initialize(), and only use the python/ folder
next to the test binary as the interpreter's home when it holds a standard
library.
2026-10-01 14:42:20 +08:00
HanifKoh 1a5bc8982d Stop Logged-Out Login Polling from Hitting the System Keychain (#15979)
With stealth mode off the home page asks for the login status every 2 s,
and while nobody is logged in that ends in clear_user_secret(), which
opened the system keychain and deleted the OrcaSlicer/Auth entry on the
UI thread every tick. A working keychain cost a D-Bus round trip per tick;
a keychain that never answers blocked the UI for 25 s per tick. It also
deleted a login another running instance had just saved, and ignored
use_encrypted_token_file, so opting out of the keychain did not help.

Remember whether this process read a secret from the store or wrote one,
and only then touch the store when a logged-out poll asks for a logout.
A logged-out instance never touches the keychain, and in encrypted-file
mode the poll no longer opens the keychain at all. A secret this process
cannot read, such as a token file encrypted for another OS user sharing
the data directory, is left alone by the poll. An explicit logout still
wipes both backends, so a token stranded by switching the token storage
option cannot sign the account back in later.
2026-10-01 14:41:50 +08:00
HanifKoh 94266c2819 Fill Settings Missing From a CLI Project From Its System Presets (#15953)
* Fill Settings Missing From a CLI Project From Its System Presets

A project saved before a printer or process option existed has no value
for it. The GUI takes such keys from the project's system preset; the
CLI left them at the option default, so e.g. extruder_clearance_dist_to_rod
sliced as 40 instead of the P1S's 33.

The CLI now resolves the project's system printer and process presets by
name and copies the keys the project lacks, skipping preset bookkeeping,
print-host keys, the extruder variant layout and keys the legacy handler
drops. PresetBundle::resolve_system_preset finds the vendor through its
manifest or preset cache, so it also works in release builds, which ship
vendors as caches only.

* Load a CLI Project's Printer and Process Settings as the GUI Does

The CLI filled only the keys a project lacked from its system preset.
The GUI builds a project preset differently: the project's values go
over the default preset, without the print-host keys, and every key
the project does not list in different_settings_to_system is refreshed
to its base system preset's current value. After a profile update the
two sliced the same project differently.

That step now lives in Preset::load_external_config, which takes plain
configs and gets the base preset from a callback, so the collection
lookup stays in PresetCollection. PresetBundle::project_different_keys
builds the kept-key set from a project's escaped entry, adding the
preset bookkeeping keys, and is used by both the GUI and the CLI.
PresetCollection::load_external_preset calls the shared step with no
change in behaviour.

The CLI now builds the project's printer and process configs with the
same step, passing the system preset from resolve_system_preset. That
drops the hand-kept skip list for print-host and variant-layout keys
and the legacy-key check: the shared step already excludes print-host
keys and maps per-variant values onto the base preset's variant layout.
The --uptodate path and a printer or process given on the command line
are left as they were.

Because the GUI's kept-key set always holds the bookkeeping keys, the
refresh runs for every project that names a system preset, so the CLI
now loads that preset's vendor on every such run.
2026-09-30 18:14:28 +08:00
HanifKoh 2769b12ce7 Give OBJ Quad and Flipped Faces the Texture Coordinates of Their Own Corners (#15977)
load_obj emits the second triangle of a quad from corners 0, 2 and 3,
but read its texture coordinates from corners 0, 1 and 2, so half of
every textured quad sampled the wrong part of the texture. The corner
indices are now passed down to where the coordinates are read.

A mesh with inward-facing triangles is flipped after loading, which
swaps corners 1 and 2 of every face. The texture coordinates were left
as they were. They are now swapped along with the corners.
2026-09-30 03:13:29 +08:00
HanifKoh 203bc63f35 Escape Project Metadata in the Project Page and Restrict Accessory Opening (#15956)
* Escape Project Metadata in the Project Page and Restrict Accessory Opening

The Project page rendered the model and profile name, author, description
and accessory file names from the 3MF as live HTML. Names, authors and file
names are now set as text, and the file list is built from DOM nodes with
bound click handlers instead of concatenated markup. Descriptions can
legitimately carry rich-text HTML, so they are rebuilt from an inert
DOMParser document, keeping only plain formatting tags, http(s) links and
http(s) images, with every other attribute dropped.

Opening an accessory from the page now only launches regular files that
lie inside the project's extracted auxiliary directory. The containment
check is a new libslic3r helper, is_absolute_path_within_root, built on
is_path_within_root so symlinks leading out of the root are rejected too.

* Tighten Project Page Description Rendering and Keep More Formatting

Link and image URLs in descriptions must now start with an http or https
scheme as written and parse as such with the URL parser. Preview images are
built as DOM nodes like the file list, and accessory names show their full
text as a tooltip.

Descriptions keep more plain formatting: del, ins, figure, figcaption, dl,
dt, dd, caption, q, abbr, kbd and wbr, plus alt, title, width and height on
images, colspan and rowspan on table cells and start on ordered lists.
Numeric attributes must be plain integers. Embedded YouTube players become
a link to the video.

* Confirm Before Opening Program Attachments and Load Only HTTPS Images

Opening a project attachment whose type runs as a program or script
(executables, installers, shortcuts, shell and PowerShell scripts, macOS
command files and apps, Linux desktop entries) now asks for confirmation
first. The check lives in libslic3r as is_executable_file_name and ignores
the trailing dots and spaces Windows strips from file names.

Images in project descriptions are kept only when they load over https,
so opening the Project tab no longer issues plain-http requests.

* Open Project Attachments Through One Guarded Helper

The Edit Project Info view launched attachments directly, without the
checks the project page has. Both now call
desktop_open_project_attachment, which checks that the file is inside
the auxiliary directory, asks for confirmation where needed and then
opens it.

The auxiliary root was built through encode_path, which returns code
page bytes on Windows, while boost::filesystem reads a narrow string as
UTF-8. With a non-ASCII temporary directory the root never matched and
no attachment opened. It is now built from the UTF-8 path directly.

The list of program extensions could not be kept complete and let
unknown types open without a prompt. It is replaced by
is_safe_to_open_file_name, a list of plain document, image, model and
video types that open directly. Everything else asks first.
2026-09-30 00:39:30 +08:00
HanifKoh e40030cf81 Stop Malformed Network Responses from Crashing the App (#15947)
* Stop Malformed Network Responses from Crashing the App

Duet, MKS and UltiMaker parsed print host replies with boost read_json
inside the HTTP completion callback with no try, so an HTML or truncated
reply threw out of the Physical Printer Test button and terminated the app,
or killed the upload queue thread. The five identical copies of the parser
(ESP3D's and Flashforge's were unused) are replaced by one shared
PrintHost::get_err_code_from_body that reports a non-JSON reply as an error.
The upload queue now catches a failing job per job, so one bad upload no
longer leaves later jobs queued forever.

Flashforge read material station slots with nlohmann value(), which throws
on off-type fields or non-object entries. The parsing moves into
Flashforge::parse_material_slots, which reads fields leniently with the
existing try_parse_json_int and skips bad entries.

UserManager::parse_json parsed the payload before its try block; the parse
now happens inside it.

* Keep UploadFinished Paired with UploadStarted When an Upload Throws

The exception from a throwing upload was caught around perform_job, so
the UploadFinished lifecycle event was skipped and plugins saw an
upload start that never finished.

The catch now sits around the upload call. The error is reported
through the job's error callback and UploadFinished is fired with an
error code, as for any other failed upload. The worker keeps running
for the next job. The started, upload and finished sequence moved to
PrintHostJobQueue::upload_job so it can be tested without the dialog.
2026-09-30 00:39:01 +08:00
HanifKoh ba468c842d Confine Updater and Plugin Archive Extraction to the Target Directory (#15957)
* Confine Updater Archive Extraction to the Target Directory

The preset updater extracted downloaded archives by appending each entry
name to the cache directory, and the network plugin installer did the same
for the plugin folder, without checking that the result stays inside it.

Move the updater's extraction into libslic3r as extract_archive_confined,
which validates every entry with is_path_within_root before writing
anything and fails the whole archive if one entry resolves outside the
target. The plugin installer now rejects such an entry the same way. Well
formed archives extract exactly as before.

* Harden Archive Extraction Against Symlinks

The plugin installer now creates a symlink entry only when its target is
relative and, joined to the link's own directory, passes
is_path_within_root, via the new is_symlink_target_within_root helper.
Before writing any entry it checks the destination with symlink_status, so
an existing symlink, dangling or not, is replaced rather than followed, and
it creates parent directories inside the existing error handling.
extract_archive_confined replaces a symlink at a destination file the same
way.

is_path_within_root now ignores a trailing separator on the root, which
previously made every path fail the check.

* Validate Plugin Symlink Targets Before Replacing Existing Files

A symlink entry's target is now read and checked before anything already
at its destination is removed or renamed aside, so an archive rejected
for its link target leaves the installed plugin files in place.

* Reject Paths with an Embedded NUL When Confining Extraction

is_path_within_root compared each component with "..", so a name such
as "..\0" passed the check. The filesystem calls stop at the NUL and
act on a shorter path than the one that was checked: a symlink target
read from a plugin archive as raw bytes was created as "..", pointing
out of the plugin directory.

A path containing a NUL is now rejected before anything touches the
filesystem, which covers every caller, including entry names taken from
the Unicode Path extra field.
2026-09-29 23:40:12 +08:00
HanifKoh e68694dbaf Percent-Encode Local File URLs for Embedded Web Pages (#15961)
* Percent-Encode Local File URLs for Embedded Web Pages

The Home tab, setup wizard, Project tab and other embedded pages were
loaded from file:// URLs built by pasting the resources path into a
string. A '#', '%' or '?' in the install path was then read as a URL
fragment, escape or query, so the pages failed to load, for example a
portable install under D:\#OneDrive showed a directory listing instead
of the setup wizard.

Add file_url_from_path(), built on wxFileSystem::FileNameToURL, and use
it wherever a local page or image URL is built from a path. Queries such
as ?lang= are appended after the path is encoded. The wizard's printer
cover images are passed to the page as file URLs too.

* Encode the Login Error Page URL and Cover More Windows Path Forms

The login dialog's error page was still loaded from a raw resources path;
it now uses file_url_from_path like the other local pages.

The Windows file URL tests now also cover a resources path joined with a
forward-slash relative path, as the callers build them, and a UNC path.

* Build the Flush Dialog Page URLs with the Shared Helper

WipingDialog and NozzleListTable still called
wxFileSystem::FileNameToURL directly. They now go through
file_url_from_path like every other local page, so the URLs are built
in one place.

Adds a test for a resources directory with a '#' in its name, which the
plugin page check did not recognise before.
2026-09-29 22:30:56 +08:00
HanifKoh 8ffd3e514e Harden OBJ and DRC Import Against Malformed Files (#15948)
* Validate OBJ Texture-Coordinate Indices

load_obj read the texture coordinates of a face without checking the
vt index, so a face referencing a vt past the end of the list read out
of bounds and crashed, and a face vertex with no vt read index -1.
Out-of-range or missing indices now fall back to a zero UV. The face
keeps its entry in the per-face UV list, so the following faces stay
aligned, and the geometry loads as before.

Negative (relative) vt indices were also rebased by dividing the float
count by 3, but each vt stores two floats.

* Reject DRC Meshes Without Positions or with Invalid Face Indices

load_drc dereferenced the POSITION attribute without checking that the
mesh has one, and trusted the decoded face indices, which the Draco
decoder does not check against the point count. Both now fail the load
cleanly. A failed vertex conversion is treated the same way.

The libslic3r tests link Draco so they can encode the malformed meshes
in-test.

* Keep OBJ Texture Coordinates That Carry a W Component

The vt parser stopped reading the optional third component when texture
coordinates were cut down to u and v, but the check that nothing is left
on the line stayed. A legal "vt u v w" line was therefore rejected and
silently dropped, shifting every later texture index. The w component is
parsed again and discarded.

The texture coordinate stride is now a named constant, OBJ_TEXCOORD_LENGTH,
used by the parser and the importer, so the relative-index rebase cannot
drift from the storage layout again.
2026-09-29 12:25:16 +08:00
HanifKoh d35ea27ea5 Validate Zip Entry Sizes Before Parsing 3MF XML (#15958)
The 3MF importers read XML entries into a single expat buffer whose size is
an int, while the archive extraction used the entry's 64-bit declared size.
The two could disagree for entries declaring more than INT_MAX bytes.

Reject such entries before allocating, and use one size for the buffer, the
extraction and the parse. This applies to the BBS importer, the PrusaSlicer
importer and the PrusaSlicer fingerprint probe. The load now fails with an
error instead.
2026-09-29 02:32:50 +08:00
HanifKoh 490d134507 Harden 3MF Loading Against Malformed Plate IDs and Paint Data (#15959)
* Reject 3MF Plate IDs Below 1 Instead of Indexing Before the Plate List

The plate importer copied each plater_id from model_settings.config into the
1-based plate list after checking only the upper bound, so plater_id="0"
wrote to plate_data_list[-1] and crashed on load. Both copy sites now reject
ids below 1 with the same "invalid plate index" error already used for ids
past the end.

* Drop Malformed 3MF Paint Data Instead of Reading Past the Bitstream

Painted facets are decoded from a bitstream a nibble at a time with no bound
check, so a truncated or corrupt paint string in a 3MF (for example split
codes with no children behind them) read past the end and crashed on load and
slice. A one- or two-side split naming side 3 also indexed past the triangle's
vertices.

Every nibble read now goes through a bounds-checked reader. Loading validates
each triangle's tree and drops a malformed one with a warning, so the stored
data, used extruder states and later decoding all agree. deserialize() also
unwinds and clears any triangle whose tree is incomplete or malformed, and
has_facets() stops at a truncated triangle. Valid streams decode unchanged.
2026-09-29 02:31:26 +08:00
HanifKoh 41eeaf3883 Sanitize Server-Supplied Download File Names (#15955)
* Sanitize Server-Supplied Download File Names

The URL downloader used the file name from the Content-Disposition header
as given, without the cleaning and unused-name search applied to the
URL-derived name.

Reduce the header name to a sanitized base name with the new
sanitize_file_basename helper, which splits on both path separators and
rejects names made only of dots and spaces. Run the result through the
same unused-name search as the URL-derived name, now shared in
find_unused_filename, and fall back to the URL-derived name when nothing
usable remains.

* Sanitize Download Names Before Choosing an Unused One

The unused-name search probed the name as given and sanitized the
result afterwards, so a name whose special characters are replaced
could be mapped onto a file that already exists.

Move the search into libslic3r as find_unused_filename, sanitize first
and probe the name that is actually written. The download marker path
is shared through download_marker_path. Restore the last tried name in
the error reported when no free name is found, and cover the search
with unit tests.

* Keep Downloads on an Unused Name Until They Complete

When the server supplied the name, the download marker stayed under the
URL-derived name, so the adopted name was not reserved against other
downloads. The final rename also replaced any file that took the name
while the download ran.

Move the marker to the adopted name before any data is written, and
check the name again right before the final rename, picking the next
free name if it is taken by then.

* Sanitize the File Name of Model Import Links

The model import took the file name from the link as given and only
avoided an existing file with a substring match on the folder listing.

Reduce the name to a sanitized base name, falling back to untitled.3mf,
choose the name with the shared unused-name search, and check it again
before the final rename.

* Handle Filesystem Errors When Finishing a Model Import Download

Choosing the final name and moving the downloaded project into place
could throw from inside the download callback. Any such error now removes
the temporary file and reports the existing import failure message.
2026-09-29 02:19:24 +08:00
HanifKoh cda1588578 Draw the Toolpaths Top-Down When the Camera Looks Down on the Print (#15883)
The segments come in print order, bottom layer first, which seen from above is back to
front: every hidden fragment is shaded before the one that covers it, and on an integrated
GPU that overdraw is most of the frame. Drawing the instances last to first whenever the
camera looks down lets the depth test reject the hidden fragments instead. Side views and
views from below keep the print order, and the shadow-caster pass is unchanged.
2026-09-28 15:36:08 +08:00
HanifKoh db10c719f6 Keep the Scarf Seam Free of Sub-Millimetre Segments (#15832)
A scarf joint split the loop at exactly the scarf length, so the remainder
of the segment the split landed in became the first flat segment, often a
fraction of a millimetre. The seam insertion also leaves segments of a few
micrometres at both ends of the loop, which a scarf extrudes through where a
plain loop would start or stop. With junction deviation the planner treats
such short moves as tight corners, limited by the Z axis acceleration at the
end of the ramp, and slows to a third of the wall speed or nearly halts at
the seam.

Extend the ramp to the next vertex when the remainder would be shorter than
half a scarf step, capped at a millimetre, beyond which planners treat a move
as ordinary; the scarf only grows, never shrinks. Drop the vertex next to the
seam point at either end when that segment is shorter than an eighth of a
common line width, so the loop still starts and ends at the seam; a trimmed
path loses its arc fitting and prints as line segments. Clamp the scarf
length to the trimmed loop so a scarf covering a whole loop still ends at
full flow. The descending pass reuses the same path, so both wedges stay
consistent, and a flat part that would collapse to a single point is dropped.
2026-09-25 12:32:05 +08:00
HanifKoh ddf9b85169 Reserve the Prime Tower When the CLI Arranges a Project (#15837)
The global arrange branch, taken by --arrange with all plates selected, only
reserved the prime tower when filament ids had been given on the command
line for STL input. A project carries its filament use per plate and its own
tower positions, but that set was empty for it, so the tower was never an
obstacle: the arranged pile was centred over it and the slice then failed on
a G-code path conflict.

When no filament ids were given, count the filaments each plate uses and
reserve a tower on every plate that needs one, keeping the project's tower
position instead of resetting it to the default. Only a tower the slicer will
print is reserved: the prime tower must be enabled, and a by-object print
gets none unless a smooth timelapse needs it, as the per-plate arrange
decides. Overflow beds are sized for the busiest plate. The STL route is
unchanged.
2026-09-25 08:39:52 +08:00
HanifKoh af52da061f Report Per-Object Slicing Errors in the CLI (#15834)
G-code generation collects errors raised per object, such as an empty
first layer, into one SlicingErrors exception whose own message is just
"Errors". The CLI's generic handler printed that word and recorded the
generic slicing error text, so a headless caller had nothing to act on.

Let Print render the per-object messages with each object's name, and have
the CLI catch SlicingErrors ahead of the generic handler, print that text
and record it as the result's error string. The exit code is unchanged. A
unit test lifts a cube off the bed and checks the message names the object.
2026-09-25 08:37:28 +08:00
HanifKoh ef956b995a Pick the Parity Build From the Unfiltered Run List and Allow Pinning One (#15823)
The nightly found its build with a filtered run listing (branch=main,
status=success) and trusted the first result. GitHub serves filtered
listings from a run search index that has intermittently returned
weeks-old results, so some nights tested a build from weeks earlier and
reported its differences as regressions. The same filter also matched
fork PR builds whose branch is named main.

The build is now picked from the unfiltered listing, which stays
current, and filtered here: a successful build_all run of this
repository on the requested branch. Fork PR builds are excluded by
repository. A feature branch is normally built only for its PR, so this
repository's own PR builds stay eligible, but a PR build compiles the PR
merged into its base rather than the head commit the later jobs check
out, so a push or dispatch build of the branch is preferred when the same
page of the listing has one. A scheduled run fails instead of testing a
build more than 48 hours old, and every run names the build it tested
in the job summary.

Manual runs scan further back, so a branch that last built weeks ago
can still be tested, and a new build_run_id input pins one build_all
run, read directly rather than through a search.
2026-09-23 12:57:15 +08:00
HanifKoh 89dc4e1b99 Add an Align to Y Axis Option to the CLI Arrange (#15836)
The CLI turned "align to Y axis" on for every i3 printer with no way to opt
out. With rotations forbidden the pre-rotation is the result, so every object
ends up turned 90 degrees from how it was loaded. The GUI defaults the
checkbox the same way for i3 printers, but lets the user untick it.

Add --align-to-y-axis. When it is not given the printer-structure rule still
applies, so existing calls are unchanged; the CLI's own options are filled
with defaults after parsing, so the keys the user typed are remembered to
tell the two apart.
2026-09-23 12:43:08 +08:00
HanifKoh 12670a6e40 Answer the Preview's Per-Frame Lookups From Cached Sums and Draw Segments From an Index Buffer (#15833)
* Answer the Preview's Per-Frame Time Query From a Cached Sum

The G-code preview's cost is linear in the number of toolpath vertices, and on a
tall multi-filament print the wipe tower dominates that count: it emits a roughly
constant 160-180 moves on every layer whatever the object is, measured at 57-61%
of all moves on a three-filament print.

Four places scanned or allocated across the whole vertex array. None of them
needed to.

get_estimated_time_at re-accumulated the estimated time from vertex 0 on every
call, and its caller is the tool marker tooltip, which ImGui re-renders every
frame while the properties panel is unfolded. It now starts from a running sum
kept at each layer's first vertex, built at load in vertex order, and adds only
that layer's vertices: the same additions in the same order, so the float result
is unchanged, at a cost of one float per layer and time mode rather than per
vertex. At the 351k vertices of a 636-layer test print the call scanned the whole
print (238us); it now scans one layer.

update_view_full_range walked from vertex 0 to find where the layer range starts,
on every slider tick. It now starts at the first vertex of that layer. The index
is derived from the vertices rather than from Layers::Item::range, because
Layers::update folds a vertex whose layer_id arrives out of order into whichever
bucket is open, which makes that range the wrong answer in general; the index
costs four bytes per layer, not per vertex.

update_colors_texture allocated one float per vertex of the whole print on every
slider tick. It now reuses a buffer.

render_legend fetched the layer Zs and the per-layer times from inside loops over
the custom G-code items, and built whole vectors only to test them for emptiness.
The times are hoisted, the Zs are built lazily so a print with no colour change
does not pay for them at all, and the emptiness tests use the existing counters.

No rendering behaviour changes.

* Draw the Preview's Toolpath Segments From an Index Buffer

The preview's frame cost is dominated by one call: a single instanced draw of
every visible toolpath segment. On a tall multi-filament print the wipe tower
supplies most of those segments, which is why the preview of a large tower is
slow and why shrinking the layer range speeds it up again.

That draw is not fill bound. Shrinking the model to about a fortieth of its
screen area moved the frame from 419 ms to 401 ms, so the cost is per segment,
not per pixel, and it is paid in the vertex shader: five texelFetch calls plus
several cross/normalize per invocation.

Each segment is a box of eight corners, but it was submitted with
glDrawArraysInstanced over a 24 entry array, so every corner was transformed
once per triangle that touches it and the shader ran 24 times per segment. The
same 24 entries are now an element buffer over the eight distinct corners, which
lets the post-transform cache reuse them and drops the shader to 8 runs per
segment. The triangles, their winding and the vertex_id each corner receives are
unchanged.

Measured over 100 frames on the 636-layer, 351k-vertex three-filament fixture,
the segment draw goes from 381 ms to 322 ms per frame. That is a software
rasterizer, where triangle setup dominates and understates the win; the drop in
shader invocations is the transferable part.

Verified by loading the same project in this build and in a build of the parent
commit and comparing the canvas across three states - the default view, a
rotated camera, and a reduced layer range: pixel identical in all three. The
rotated case matters because the shader picks its corner offsets from the camera
direction. The only pixels that differ anywhere on screen are in the G-code text
panel, which prints a per-process object id that varies between any two runs.
2026-09-23 12:06:02 +08:00
HanifKoh 19e094a1d1 Move the Cube with the Wipe Tower in the Profile Validator (#15799)
The slice check centres its cube on the bed, puts the prime tower beside
it, then pulls the tower alone inside the printable outline. On a bed too
narrow for the estimated footprint that pull drags the tower back over
the cube: Volumic EXO42 IDRE MIRROR MODE (189 mm wide, 87.6 mm estimate)
logged "gcode path conflicts found between WipeTower and cube" in every
run, and three ~105 mm beds were left with 0.15 to 3.3 mm of clearance.

The cube and the tower's footprint are now pulled inside as one rigid
pair, so the clearance between them is fixed by construction. A bed too
small for the pair keeps the old placement, and presets that were never
clamped keep their exact layout.
2026-09-22 18:22:42 +08:00
HanifKoh 2876374b45 Add a Dockable HTML Panel API for Plugins (#15736)
orca.host.ui.create_dock_panel(html, title, width, height, on_message,
on_close, dock) hosts plugin HTML in a pane of the Plater's dock manager,
next to the sidebar, and returns a UiDockPanel handle
(post/show/hide/close/is_open). The arguments follow create_window(). The
panel uses the window.orca bridge of plugin windows, restores its position
and size from the saved window layout, hides with the Plater off the Prepare
and Preview tabs when floating, and is closed with its plugin; plugin panes
are removed in MainFrame::shutdown().

The web view hosting moves out of PluginPage into a shared WebPanel base:
bootstrap page and swap to the plugin HTML, theme, element-default and
bridge scripts, window.orca message parsing, delivery to the page, and live
re-theming, also re-applied on every load after the swap. Pages tabs and
docked panels both derive from it. Pages tabs now re-theme in place on a
theme change instead of being reloaded, and a window.orca call a host does
not support is logged.

What the hosts share no longer lives in one of them: the bootstrap page, the
base URL and the plugin-window bridge move to Widgets/WebHosting, used by
WebDialog and WebPanel alike. The Plater restores plugin panes with a new
saved-layout parser, GUI/AuiPaneLayout, kept in its own small header so
slic3rutils can test it without pulling in the Plater.

The web hosting classes carry no plugin name, so other hosts can reuse them:
PluginWebDialog becomes WebDialog (its bootstrap page moves to
resources/web/dialog/WebDialog), and destroy_for_plugin(),
load_plugin_content() and plugin_defaults_user_script() become
destroy_silently(), load_page_html() and element_defaults_user_script().

Includes a sample plugin (sandboxes/orca_dock_panel_plugin_any.py) and
binding and layout-helper tests in slic3rutils.
2026-09-22 14:52:25 +08:00
HanifKoh 8b63628cf9 Restore Plugin HTML After a WebKit Reload (#15737)
Plugin dialog content is injected with SetPage, so on the WebKit backends a
reload (context menu, keyboard shortcut or location.reload()) re-fetches the
SetPage base URL instead of the injected document, and the plugin UI is gone
for good: load_plugin_content() returned early once m_content_loaded was set.

Re-inject the plugin HTML when a main-frame load after the initial swap is
neither that swap nor a page the plugin linked to. m_own_page_load marks the
load our own SetPage caused, and the URL test recognises the reload: the
injected document and the directory a reload re-fetches both report the base
URL, so a load of any other URL is left alone. The test ignores a fragment the
page navigated to, and undoes the escaping the web view applies to what the
resources path holds.

A load reaching the base URL is not enough on its own, because WebKitGTK reports
a navigation that never committed against the document that stayed and then
finishes that document again: a link to a missing file therefore arrives as a
load of the base URL and reads exactly like a reload. So the re-injection also
requires a navigation to the base URL to have committed, which a reload always
does and a failure never does.

A bootstrap page that cannot be loaded is still not recovered from: WebKitGTK
substitutes a stock error page for it, and that load supersedes the swap
whichever way the swap is ordered around it. The file ships, so this is a
broken-install path; nothing here makes it worse than it already was.

No separate MSW path is needed: wxWebViewEdge ignores the SetPage base URL, so
its documents report about:blank and the test never matches there, and WebView2
reloads NavigateToString content from its own history entry anyway.
2026-09-21 17:09:05 +08:00
HanifKoh 0956b4d8fe Add a Nightly Parity Workflow (#15712)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Adds a nightly workflow that runs the long parity checks from
[orca-test-repo](https://github.com/OrcaSlicer/orca-test-repo), which
are too slow for the per-build "Run external slicer regression tests"
step and are kept out of every PR and merge build.

## What it runs

`.github/workflows/parity_nightly.yml`, three jobs:

| Job | What it does |
|---|---|
| Find the build to test | Picks the latest successful `build_all.yml`
run for the branch (`main` by default) and records its commit. |
| Override sweep effect stage (shard 0 and 1) | Runs orca-test-repo's
override sweep with `--effect-full`: every config option that lands on
the CLI is re-sliced on its own to check that it actually changes the
G-code. Split into 2 shards, each with a 60-minute timeout. |
| GUI-vs-CLI parity harness | Slices a set of fixtures in the GUI
(headless under Xvfb) and on the CLI, compares the exports, and scores
divergences against a known-differences ledger. It reports only and
never fails on a divergence. |

## When it runs

- **Every night at 21:00 UTC,** after `build_all.yml`'s 17:00 UTC run
has finished.
- **By hand** through `workflow_dispatch`, with optional inputs:
  - `build_branch`: the branch whose latest successful build to test;
  - `test_repo_ref`: the orca-test-repo ref;
  - `fixtures`: a subset of harness fixtures;
  - `cli_presets`: `flat` or `raw`.
- **No `push` or `pull_request` trigger,** so nothing here runs on PRs
or merges. The per-build CI step is unchanged.

## How it tests a build

- **Binary:** the Linux x86_64 AppImage from the chosen `build_all` run.
- **Source:** OrcaSlicer checked out at that run's exact commit. The
AppImage only ships packed preset caches, so profiles and the CLI option
list come from this checkout, matched to the binary.
- **Output:** each job writes a summary to the run page and uploads its
report (`override-report-shard*`, `parity-scorecard`) for 30 days.
- **Failures:** a failing effect shard fails the run, and GitHub's usual
failure notification for scheduled workflows applies.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

- Dispatched on this branch against orca-test-repo `main` and #15693's
build ([run
34933239912](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/34933239912)).
Every job passed:
- **effect shard 0:** 19m31s; 225 options sliced, 151 effective, none
crashed or hung, every fixture sliced;
  - **effect shard 1:** 19m33s; 349 options sliced, 254 effective, same;
  - **harness:** 4m32s; all 13 fixtures, 0 new divergences, 0 errors.
- An earlier dispatch on this branch, testing #15693's build against
orca-test-repo's parity branch ([run
34836468900](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/34836468900)),
passed: effect shards in 20m28s and 23m30s, and the harness reported 0
new divergences.
- The workflow only runs from the default branch on its schedule, so the
nightly trigger itself takes effect once this is merged.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-15 15:01:35 +08:00
HanifKoh 5514559feb Load Each Vendor Tree Once When the CLI Resolves System Presets (#15693)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Since #15438, every CLI run that loads a system preset spends about a
second per preset file re-parsing that vendor's entire profile tree. A
slice with a machine, process and filament preset got roughly 2.5 s
slower, and a four-filament slice roughly 4 s slower. This PR loads each
vendor tree once per run instead. Resolved presets and G-code are
unchanged.

The GUI never takes this path, and no release contains #15438, so the
regression only affects CLI runs on current dev and nightly builds. That
includes print farms, slicing services and plugins that call
`orca-slicer --slice`, and CI suites.

## Changes

### Why it was slow

`PresetBundle::resolve_preset_config` resolves a system preset through
its vendor manifest by loading the whole OrcaFilamentLibrary bundle and
the whole vendor tree from JSON, then picking the one preset out. The
CLI did that separately for every `--load-settings` and
`--load-filaments` file, on a fresh `PresetBundle` each time. With BBL
presets, a machine + process + filament run opened `BBL.json` three
times and read BBL's 2,879 profile files and the library's 512 three
times over.

### Load each vendor tree once

- `PresetBundle` keeps every vendor bundle its manifest path loads,
keyed by source root, vendor and substitution rule, and reuses them for
later resolutions on the same bundle.
- OrcaFilamentLibrary is cached the same way, so vendors under one root
share a single library load and the library's own presets resolve from
that same instance. A vendor bundle only reads from its base while
loading, so sharing it is safe.
- A failed or throwing load is not kept, so error reporting is
unchanged.
- The key includes the source root, so presets from two different
profile roots still resolve separately.
- The CLI resolves every system preset through one `PresetBundle` for
the whole run, instead of creating one per file.

The resolved configurations still come from the same canonical vendor
loader, so what a preset resolves to does not change. Only the CLI calls
`resolve_preset_config`, so a long-lived GUI bundle cannot end up
holding profile trees that later change on disk.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

CLI slice of a 20 mm cube with X1 Carbon system presets. Both builds get
the same datadir, best of 3, Linux. "Before" is this PR's base from CI.

| System presets loaded | Before | After |
|---|---|---|
| machine | 0.95 s | 0.87 s |
| machine + process | 1.67 s | 0.92 s |
| machine + process + 1 filament | 2.51 s | 0.97 s |
| machine + process + 4 filaments | 5.00 s | 0.99 s |

Files opened during the machine + process + filament run (`strace -e
openat`):

| | Before | After |
|---|---|---|
| `BBL.json` | 3 | 1 |
| `OrcaFilamentLibrary.json` | 3 | 1 |
| `system/BBL/**/*.json` | 8,634 | 2,880 |
| `system/OrcaFilamentLibrary/**/*.json` | 1,536 | 512 |

Peak memory did not rise: max RSS 306 MB → 286 MB for the three-preset
run, and 305 MB → 285 MB for four filaments. The "before" figure is an
AppImage, so part of that gap is probably packaging.

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

- New test "Manifest-backed resolution reuses the vendor tree it already
loaded" in `tests/libslic3r/test_preset_bundle_loading.cpp`. It resolves
one preset, changes the parent profile on disk, then resolves a sibling.
The same bundle returns the value it already loaded, and a fresh bundle
picks up the change.
- New test "Manifest-backed resolution shares the library between
vendors under one root". It resolves through one vendor, changes a
library profile on disk, then resolves through a second vendor and a
library preset on the same bundle. Both return the value already loaded,
and a fresh bundle picks up the change.
- All `[Preset][Bundle]` tests pass (87 test cases, 1,069 assertions),
including the existing manifest-backed resolution cases for source-root
scoping, malformed vendor loads, missing parents and type mismatches.
- G-code of the three-preset slice is identical before and after, header
lines excluded.
- The external CLI regression suite passes. Two cases report as
unexpectedly passing because #15639 fixed their bug. They pass the same
way on this PR's base without the change.
- A GUI-vs-CLI parity run over 10 fixtures shows no new differences.
- Builds clean on Linux (Release, with tests).

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-15 15:01:24 +08:00
HanifKoh 37e2b6c928 CLI: let --export-settings - write the merged config JSON to stdout (#15698)
`--export-settings` already writes the merged config as JSON at the
right point in the CLI flow. Passing `-` now writes that same document
to stdout, so scripts can inspect the effective config without a temp
file. This replaces #14605.

- `ConfigBase::save_to_json` gains a stream overload. The file overload
serializes through it before opening the file, so the output format is
unchanged, and a config that cannot be serialized (invalid UTF-8) now
leaves the existing file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
of ending the process with an uncaught `type_error`. Files keep the
strict behaviour.
- To keep stdout pure JSON, `-` is rejected up front (stderr message,
`CLI_INVALID_PARAMS`, shell status 254) when combined with an action or
transform that can write to stdout or does real work: `--info`,
`--help`, `--orient`, slicing and exporting. Options that do nothing
without a slice (`--uptodate`, `--min-save`, `--pipe`, ...) are still
accepted.
- The one unconditional stdout write on a success path, "skip locked
instance" during arrange, now goes to the log.
- Every other value, including the default `output.json`, behaves as
before.

Tests in `tests/libslic3r/test_config.cpp`: the stream output equals the
file output and keeps the tab-indented format; invalid UTF-8 throws on
the strict path and is replaced when asked; a failed save leaves the
previous file intact.
2026-09-15 13:16:22 +08:00
HanifKoh 00429da739 Apply the GUI's Mixed Filament Rules on the CLI (#15636)
A valid mixed filament already slices the same on the CLI as in the GUI;
these are the places where the CLI still skipped a rule the GUI applies.

- Keep the prime tower when a mixed filament is used, even if every
  --load-filaments preset is the same. A mixed filament swaps between its
  components every layer, so turning the tower off left the swaps with
  nothing to purge on.
- Leave a mixed slot's row and column of the flush matrix at zero when
  --filament-colour triggers a recompute, as the GUI does; a mixed slot
  never reaches a nozzle.
- Refuse a mixed slot that has no filament of its own. Feature filament
  ids aimed at it were past the filament count, got reset to filament 1
  and the model silently printed in one colour.
- Refuse a plate that uses a mixed filament whose components are
  different filament types, the type half of the GUI's
  Sidebar::has_broken_mixed_filament. Missing or out-of-range components
  are already rejected for the whole project by validate().
  get_extruders_under_cli gains an expand_mixed_slots flag so the gate
  can see mixed slots rather than their components; existing callers
  keep the expanded list.

Both refusals exit with the new CLI_MIXED_FILAMENT_INVALID (-69).
2026-09-14 14:28:08 +08:00
HanifKoh 31f6eb2718 Keep the First Value When a Per-Filament Variant Option Is Too Short (#15639)
update_values_to_printer_extruders_for_multiple_filaments picks each
filament's value from the flattened (filament x variant) columns of every
per-filament variant option. When a column index fell past the end of the
option's values, it skipped that filament and left the zero the output
vector was created with.

The GUI always hands this function full columns, but the CLI does not:

- a CLI override of a single value, such as --nozzle-temperature=211 on a
  four-filament project, came out as 211,0,0,0, so three filaments would
  print at 0 C;
- loading fewer filament presets than the project has filaments left the
  remaining filaments' columns missing, so filament_cooling_before_tower
  came out as 10,10,0,0 and filament_ramming_volumetric_speed as -1,-1,0,0.

An out-of-range column now keeps the option's first value, the fallback
get_at() and the sibling gather step already use. The seven per-type copies
of the loop are replaced by that same gather_option_values helper, moved
above the function; it now takes its caller's name for its log lines. An
empty option, which has no first value, is given one registered default per
filament first; it used to be replaced with zeros.

On a partial load a filament whose preset was not loaded takes the first
filament's value rather than its own preset's, which the CLI does not load;
for the options seen in practice those agree.
2026-09-14 14:26:32 +08:00
HanifKoh f21f062ded Cache compiled objects between CI runs (#15611)
# Description
<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Every CI build leg compiles the whole tree from scratch: 42 to 57
minutes of each build job, on every push and every pull request, roughly
200 runs a week. This PR caches the compiled objects with ccache so that
a run only compiles what changed since the last push to main. With a
warm cache the compile steps take 1 to 4 minutes on all six legs and a
pull-request run finishes in about 30 minutes instead of 75.

Three prerequisites landed last week and made this measurable: #15537
took `GIT_COMMIT_HASH` off the compile line, #15552 made a build without
the precompiled header work on Windows, and #15501 stopped the Flatpak
job from rebuilding its dependencies.

## Changes

### Compiler cache in `build_orca.yml`

Each build leg (Linux x86_64/aarch64, Windows x64/arm64, macOS
arm64/x86_64) restores a cache entry keyed by that leg, compiles through
`ccache` via `CMAKE_<LANG>_COMPILER_LAUNCHER`, and prints its hit
statistics at the end of the job. The macOS universal combine does not
compile and is left out.

Who writes the cache is the important part. Cache entries are immutable
and a restore always takes the newest matching one, so every save is a
new entry that is never read again once a newer one exists. Therefore:

- **Pushes save.** After a successful save, the older entries for the
same leg on the same ref are deleted, so a branch holds exactly one
entry per leg. The save comes first, so a failed save leaves the
previous entry in place.
- **Pull requests restore only.** They read main's entries (GitHub lets
a PR read the base branch's caches) and keep nothing. Saving from PRs
would add about 6 GB per run that no other run can read.

The store is therefore a flat ~7 GB (one entry per leg: Linux ~1 GB,
Windows ~2 GB, macOS ~0.6 GB), not a growing one. The
`hendrikmuhs/ccache-action` only installs and configures ccache; restore
and save go through `actions/cache` with one path string, because the
cache service only matches entries saved under the identical path and
the action spells it differently on Windows. A failed ccache install
falls back to an uncached build rather than failing the job.

### Precompiled header off when the cache is on

With `SLIC3R_PCH` left on, a warm cache hit only 19 % of compiles: Clang
stamps the PCH with the build time, CMake does not pass
`-fno-pch-timestamp`, and everything that includes the PCH (libslic3r
and libslic3r_gui, ~750 files) missed every run. `build_linux.sh -p`
exists for exactly this reason. The workflow now exports
`ORCA_EXTRA_BUILD_ARGS=-DSLIC3R_PCH=OFF` whenever ccache is enabled,
which brings the warm hit rate to 98.4–98.9 %.

The cost is on cold compiles, which are 25–60 % slower than today's PCH
build (ccache preprocesses every miss before compiling it, and the miss
compiles without PCH). Main pays this once after an image update or a
wide header change; PRs pay it only for the files their change
invalidates. A change to a header included by half the tree
(`PrintConfig.hpp`, `Preset.hpp`, `Model.hpp`) lands a run at 1.2–1.9×
today's time. `ccache`'s depend mode would remove the preprocessor pass
and is the natural follow-up.

### Includes the precompiled header was supplying on macOS

A build without PCH had never been tried on macOS. Three files used what
`pchheader.hpp` happened to include: `LocalesUtils.cpp` needs
`<sstream>` and `<iomanip>`, and `AmsMappingPopup.cpp` /
`PhysicalPrinterDialog.cpp` need `<wx/tooltip.h>`. libstdc++ and the GTK
wx port pull these in transitively; libc++ and the Cocoa port do not.
This is the macOS counterpart of #15552 and is worth merging on its own.

### `ORCA_EXTRA_BUILD_ARGS` pass-through

`build_linux.sh` already forwarded this variable to the slicer
configure. `build_release_macos.sh` now reads it into an array
(shellcheck-clean), and `build_release_vs.bat` appends it on both
configure lines, so CI can add a CMake option without editing three
scripts.

## Behaviour reviewers should know about

- **Main-only cache writes need `actions: write`** on the workflow token
to delete the previous entry. The default token already has it (the
nightly deploy steps write with it), so no `permissions:` block was
added. A fork PR's read-only token never reaches the delete step.
- **A runner image update cold-starts the cache** as configured, because
ccache keys the compiler by its mtime and every image rebuild reinstalls
it. Images updated 20260819 → 20260828 during this work, about every one
to two weeks. Keying on the compiler version string (`compiler_check`)
would avoid that; left as a follow-up since it changes every hash.
- **What is now the critical path:** the two Flatpak jobs (46–66 min,
untouched here), the orca-test-repo regression suite run inline in the
Linux job (7 min), and NSIS/PDB/MSIX packaging on Windows (6 min). Those
are the next wins.
- **Open question:** CI still drives `build_release_vs.bat`. #15552 gave
`build_win.bat` a `--cache ccache --no-pch` option; moving the Windows
job onto it would replace the batch-file change here.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

Compile step of each build leg, minutes. Main's numbers are from run
34324625046.

| Leg | main | cold, PCH on | warm, PCH on | cold, PCH off | warm, PCH
off | ~50 % of headers changed | 5 source files changed |
|---|---|---|---|---|---|---|---|
| Linux x86_64 | 48 | ~75 | (19 % hits) | ~110 | **2.6** | 91.3 (452/928
misses) | 3.4 |
| Linux aarch64 | 41.7 | 53.4 | 52.2 (178/927 hits) | 58.8 | **2.5** |
55.8 (452/928) | 2.9 |
| Windows x64 | 57 | 85.5 | — | ~105 | **2.4** | 76.8 (449/974) | 2.5 |
| Windows arm64 | ~45 | 64.4 | — | ~78 | **4.3** | 59.6 (450/974) | 4.2
|
| macOS arm64 | 51 | ~71 | — | — | **0.8** | 79.7 (453/947) | 0.9 |
| macOS x86_64 | ~43 | 70.2 | — | — | **1.0** | 68.1 (411/742) | 1.0 |

Warm hit rates: 98.4–98.9 % on every leg; the 11–14 misses are what any
commit changes (version stamp and its includers). The "50 % of headers"
column is a real event: #15251 and #15416 merged into main between two
runs, changing 20 headers that reach 453 of 870 translation units.

Whole run, before and after (a pull-request run; wall clock to the last
non-Flatpak job):

| Job | main (run 34324625046) | warm cache (run 34444305385) | what
remains |
|---|---|---|---|
| Windows arm64 | 50.0 | 15.7 | compile 4.3, NSIS 3.5, cache save 1.6,
deps restore 1.1, cache restore 1.0 |
| Windows x64 | 67.4 | 13.2 | NSIS 3.2, PDB 2.6, compile 2.4, MSIX 0.5 |
| Linux x86_64 | 57.5 | 12.6 | orca-test-repo regression 7.6, compile
2.6 |
| macOS x86_64 | 46.9 | 6.1 | free disk space 2.3, compile 1.0 |
| Linux aarch64 | 43.9 | 4.6 | compile 2.5, apt 0.9 |
| macOS arm64 | 54.9 | 4.4 | free disk space 1.7, compile 0.8 |
| macOS universal | 7.7 | 2.2 | signing and notarisation only on main |
| Flatpak x86_64 / aarch64 | 66.6 / 46.5 | unchanged | full compile
inside flatpak-builder |
| **Wall clock** | **75 min** | **31 min** (Flatpak excluded; 66 with
it) | macOS runner queueing now exceeds job time |

Cache storage: one generation per leg is 400–680 MB compressed at PCH
on, 0.6–2 GB at PCH off; six legs ≈ 7 GB. Without the delete step, 21
main pushes a week would hold ~80 GB of entries that are never read.

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

- Thirteen CI runs on this PR, one change per run, with the ccache
statistics printed by every leg: cold (34336272234), warm with PCH
(34346737197), cold and warm without PCH (34352209577, 34364791732), the
macOS include fixes (34435044411, 34435968806 with `ninja -k 0` to list
every remaining file, 34439532375), all legs warm (34444305385), the
keep-only-newest cleanup (34450381312, then 34452640274 after the
Windows CRLF fix), the half-tree invalidation (34452640274), the
five-file change (34463517683, 34464720539), and this final shape
(34466377763, restore-only).
- Unit tests on all five platforms, the profile slice check, the Windows
build-script suite, Shellcheck and the universal DMG build all pass on
the cached binaries.
- The cleanup was verified against the PR's own cache scope: 44 entries
from the earlier runs reduced to exactly one per leg, on all three
platforms, after fixing the CRLF that made `gh cache delete` fail on
Windows.
- A libc++ syntax-only pass over all 1986 C++ translation units on Linux
found the `LocalesUtils.cpp` include; the two wx includes only surface
in a real macOS build and were found with a keep-going build in one
round.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-12 00:46:24 +08:00
HanifKoh 8d874cdc36 Register Instance Copies and Moves with Their Plate (#15613)
# Description

Each plate keeps a registry of the instances it holds
(`PartPlate::obj_to_instance_set`). The plate's filament list
(`get_extruders`), its wipe tower preview and position clamp, the object
list grouping and the saved project's per-plate instance list all read
it. Two paths left it stale:

* `Plater::increase_instances` (the `+` key / toolbar) adds the copy to
the model but never registers it with any plate.
* `GLCanvas3D::do_move` (drag release and arrow keys) ended with
`notify_instance_update(-1, 0)`, so only instance 0 of each selected
object was re-registered. Rotate, scale and mirror already notify every
instance.

So a copy created with `+` and dragged onto another plate stayed unknown
to that plate: the project saved afterwards listed it on no plate, and a
multi-filament copy moved onto a single-filament plate drew no wipe
tower there and never got its tower position clamped. The Print side
selects instances by geometry, so the plate still sliced, which is why
this went unnoticed.

This PR

* registers new copies with their plate at creation;
* has `do_move` notify exactly the instances it moved (every instance of
the object when a part was moved in Volume mode), rather than instance 0
or all instances - notifying an instance that stayed put invalidates its
plate's slice result, so `(-1, -1)` as used by rotate would have
un-sliced every plate holding a sibling copy;
* drops the registry entry again when `decrease_instances` removes a
copy.

A second commit finishes the switch #15532 started with
`contain_any_instance_totally()`: `get_extruders_without_support()`,
`check_single_extruder_mixed_filament_risk()` and
`check_compatible_of_nozzle_and_filament()` still tested instance 0
only, so an object whose copy - not its original - sits on the plate was
skipped by all three.

No new options, no format change. The `is_new` flag is deliberately not
passed for the copies: a copy landing on a spiral-vase plate gets the
same "apply spiral mode settings?" prompt a dragged instance gets,
instead of a silent rewrite of the object's settings.

# Screenshots/Recordings/Graphs
Before:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/3cf9f5a9-1a4e-41e8-8c57-578f849d8c29"
/>

After:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/1b801a7e-b7cd-4ffb-bd1d-b701f90dade6"
/>


## Tests

Re-run after the rebase, both binaries driven through the same headless
harness (Xvfb 1920x1080, llvmpipe) on the same fixture: `cubeA`
(filament 1) alone on plate 1, `cubeB` (a two-part object, filaments 2
and 1) alone on plate 2, so plate 1 shows no wipe tower at load. Select
the plate-2 object, press `+`, walk the copy onto plate 1 with 36 x Left
(10 mm per press, one `do_move` each), save, slice plate 1.

Before is main `8af92214d0` - i.e. with #15532's
`contain_any_instance_totally()` already in place, so the only
difference is this PR.

* **Before:** the saved `model_settings.config` lists plate 1 with
`cubeA` only and plate 2 with `cubeB` instance 0. The copy (instance 1)
is listed **on no plate at all**, and plate 1 draws no wipe tower even
though a two-filament object is sitting on it.
* **After:** plate 1 lists `cubeA` **and** `cubeB` instance 1; plate 2
still lists instance 0. The plate-1 tower preview appears, and slicing
plate 1 succeeds with the tower actually generated - the filament panel
reports 1.10 m / 0.48 m in its Tower column over 51 filament changes,
and the G-code carries `EXCLUDE_OBJECT_END NAME=cubeB.stl_id_1_copy_0`.

Same camera and fixture on both runs, so the screenshots above are
directly comparable.
2026-09-11 11:18:36 +08:00
HanifKoh 8af92214d0 Extract and Unify Wipe Tower Estimation (#15532)
# Description

The pre-slice wipe tower size estimate existed twice:
`Print::wipe_tower_data()` (validation) and
`PartPlate::estimate_wipe_tower_size()` (GUI placement clamp, default
placement, preview, arrange, CLI placement) — with a third partial copy
in the CLI, which resolved the brim itself around the second. They were
hand-written twins reading their inputs from different places, so
validation could measure a tower with one number after the clamp had
placed it with another.

This PR extracts the estimate into one function,
`estimate_wipe_tower_footprint()` in
`src/libslic3r/GCode/WipeTowerEstimate.{hpp,cpp}`. It takes a
`ConfigBase&` (static `PrintConfig` and GUI/CLI `DynamicPrintConfig`
both work), the filament count, layer height and tallest object height,
and returns width, depth, height and the resolved brim width.
`Print::wipe_tower_data()` and a new
`PartPlate::estimate_wipe_tower_footprint()` become thin adapters around
it; `PartPlate::estimate_wipe_tower_size()` had no callers left and is
deleted.

**Inputs made to agree** — sharing the arithmetic is not enough when
each caller derives the inputs from its own view of the model:

* **Layer height** — thinnest layer among the objects on the plate,
resolved per object (Print used the first object's, PartPlate the
preset's).
* **Objects setting the height** — `PartPlate::get_extruders` counts an
object if *any* instance is on the plate, matching `PrintApply` (it only
looked at instance 0).
* **Height per object** — per on-plate instance, from the cached convex
hull (same z extent as the mesh). `PrintObject::size()` still measures
the model's first instance, so objects whose instances differ in scale
or x/y tilt can still disagree; that is inherent to the two data
sources.
* **Wipe tower filament** — counted for every caller, since
`Print::extruders()` adds it to the tool ordering even when unused.
* **Rib width cap** — kept for both (Print lacked it).
* **Config source** — everything read from the config passed in
(PartPlate read `m_print->config()`, stale on fresh plates and in the
CLI).
* **Dual-nozzle test** — `nozzle_diameter.size()` from the config for
both.

**One decision about whether a tower exists.** The rectangle branch
sized a tower the generator never builds while the rib branch reported
none for one it does; with rib as the shipped default, a single-filament
plate that still prints a tower (custom G-code tool changes) validated
against depth 0, collapsing the collision/exclusion hull to a point. The
purge volume is computed first, and an empty footprint is returned only
when nothing is purged, there is no tool change, and nothing else puts a
tower on the plate. The reason a single config cannot see arrives as a
resolved input: validation counts `Print::extruders(true)`.

A raft is deliberately **not** one of those reasons.
`DynamicPrintConfig::normalize_fdm_2` clears `enable_prime_tower` for a
plate that purges one filament unless smooth timelapse or wrapping
detection is on, and `Print::apply()` runs it, so a raft alone leaves no
tower to reserve for. (It also keeps the tower for a single *mixed*
filament, which this does not model — `Print::extruders(true)` does not
expand mixed filaments.)

**Two implementation notes:**

* Enums are read **by value**: a preset-built `DynamicPrintConfig` holds
`ConfigOptionEnumGeneric`, so a `dynamic_cast` to `ConfigOptionEnum<T>`
is null for exactly the config the GUI and CLI pass. The tests build
their configs the way `PresetBundle::full_config()` does, so that
storage is what gets tested.
* `PartPlate::estimate_wipe_tower_footprint()` is CLI-reachable, so
`get_extruders(bool)` gained a config-taking core with the identical
body; the GUI wrapper passes the app's presets, the adapter passes the
config it is given. `get_extruders_under_cli()` was not substituted: it
filters the plate's instances differently (skips unprintable ones, keeps
ones the plate flags as outside), so the GUI's filament set would have
changed in edge cases.

**Also fixed here:** `WipeTowerData` carries the effective width — set
by the estimate, and then by both planners at generation, so it never
disagrees with its neighbour `depth`; the preview and the containment
check take *whether there is a tower at all* from the footprint instead
of each re-deriving it; the config-taking `get_extruders` answers for an
object-less (`.gcode.3mf`) plate the way the wx overload does; the
preview takes body *and* brim from the plate's own footprint (an auto
brim drew every plate with the selected plate's brim);
`estimate_wipe_tower_polygon` builds its margin from the resolved brim
("Auto" gave a margin of 0) and no longer calls `std::clamp` with `hi <
lo`; the estimate falls back to declared defaults instead of hand-copied
constants. `estimate_wipe_tower_size()` /
`estimate_wipe_tower_polygon()` lose four parameters every caller took
from the same config.

## Behaviour changes reviewers should know about

G-code is never affected; no 3MF, profile or string changes. But this is
**not** a pure refactor:

1. **Validation now reserves what the placement clamp reserves**, which
is in places larger than before. A saved 3MF with a tower close to an
exclusion area or the rear edge can be rejected where it previously
sliced; dragging resolves it since the clamp agrees. Nothing re-clamps a
stored position on load (out of scope; the CLI side lands with #15518).
2. **`PartPlate::get_extruders` counts any-instance-on-plate**, which
reaches every caller of it, not only the estimate. It is `PrintApply`'s
rule and closes a GUI/CLI divergence.
3. **`estimate_wipe_tower_polygon`'s rear/right bound is looser by one
brim width** (it subtracted the brim twice).
4. **A single-filament plate with a rib wall no longer reserves a
phantom tower.**
5. **A single-filament plate whose tower comes from wrapping detection
is now validated against the bed.** Neither the old estimate (which read
only the wall type and smooth timelapse) nor the old containment gate
(the filament count or smooth timelapse) knew about that tower, so
between them it was never checked. It is printed, so it can be rejected
now.

Not addressed: the estimate still does not read `wipe_tower_type` or
per-filament `filament_prime_volume`, inherited unchanged from both
copies (the generated Type 1 tower is ~10 mm larger than the estimate on
Bambu profiles). #15516 mirrors the planners and folds into this
function on rebase.

## Verification

Before/after on the same fixtures with a main build and this branch, all
numbers read from the CLI (details, method and the real-tower and
arrange-clamp tables in the first comment):

| Fixture (divergence) | Side | Before (w × d, mm) | After (w × d, mm) |
|---|---|---|---|
| control | GUI/CLI · validation | 23.585 × 23.585 · 23.585 × 23.585 |
same |
| per-object layer 0.1 | GUI/CLI · validation | **23.585** · 31.637 |
**31.638** · 31.637 |
| unused `wipe_tower_filament` | GUI/CLI · validation depth | **39.332**
· 44.542 | **44.541** · 44.542 |
| tall object, instance 0 on another plate | GUI/CLI · validation |
**23.585** · 29.391 | **29.390** · 29.391 |
| rib cap binds | GUI/CLI · validation | 11.170 · **13.910** | 11.170 ·
**11.170** |

Before, the two estimates disagree on every divergence fixture; after,
they agree to the 0.001 mm bisection resolution, the control is
unchanged, and G-code is byte-identical. GUI screenshots of the preview
on both binaries are in the same comment.

The table was measured on the first commit; none of its fixtures uses a
raft or a zero purge volume, so the second commit does not move them.
G-code equivalence was re-checked on the final tip: `Cube.3mf` sliced by
a `main` build and by this branch is byte-identical.

# Screenshots/Recordings/Graphs
Before:
No Wipe Tower Preview:
<img width="2068" height="871" alt="image"
src="https://github.com/user-attachments/assets/7875a944-b8db-4bbc-b380-e8188a45caa7"
/>

After:
Has Wipe Tower Preview:
<img width="2551" height="882" alt="image"
src="https://github.com/user-attachments/assets/b4413695-4f24-4aa3-bae4-57304e8b7865"
/>

**Per-object layer height reaching the preview.** One object with a 0.1
mm override against a 0.2 mm preset. `main` sizes the previewed tower
from the preset, so it is smaller than the one validation reserves and
the one that prints; this PR sizes it from the object. Captured
headlessly on both builds from the same project, top view:

<img width="1408" height="596" alt="D_per_object_layer_height"
src="https://github.com/user-attachments/assets/989920fc-9f14-4658-8a3d-681c92a7f754"
/>

Measured over the four evidence fixtures on both builds, this is the
only one of the corrected inputs that changes what is drawn: the others
(an object contributing through a non-zero instance, an unused
`wipe_tower_filament`) change the estimate by amounts confirmed through
the CLI bisection above, but leave the rendered tower pixel-identical.
Arrange is unaffected either way — the tower enters the arranger as a
fixed obstacle (`m_unselected`), so it never moves.



## Tests

* `tests/libslic3r/test_wipe_tower_estimate.cpp` (10 cases / 104
assertions): rectangle and rib sizing, stability floor and auto brim,
single-filament cases (timelapse, wrapping, and a raft *not* reserving
one), a tool change reserving the floor when the purge volumes resolve
to zero, both wall types agreeing on tower existence, dual-nozzle
volume, the shipped flush-matrix path, default fallback for a missing
key, and a {rectangle, cone, rib} × {type1, type2} matrix asserting a
preset-shaped `DynamicPrintConfig` and a static `FullPrintConfig` give
the same footprint.
* `tests/fff_print/test_wipe_tower.cpp`: what `Print` feeds the
estimator — thinnest object layer height, effective width reaching
validation, the width staying current through generation, a
single-filament plate reserving a tower only when one is really printed
(raft no, smooth timelapse yes), and a wrapping-detection tower being
bed-validated. The last two fail on `main` and on the first commit of
this PR.
* Full suites green on this branch: `libslic3r_tests` 342 cases / 58325
assertions, `fff_print_tests` 174 cases / 3152 assertions. `--target
all` builds clean (including `OrcaSlicer_profile_validator`, which needs
`-DORCA_TOOLS=ON`). No new warnings.
* CLI evidence run above; its unused-`wipe_tower_filament` fixture is
also the regression check for the adapter under the CLI, which no unit
test can reach (`PartPlate` needs a GL context).

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-09 17:46:20 +08:00
HanifKoh 8a291f9d56 Confine config import to the preset directory (#15608)
import_presets reduced each zip entry to a basename by stripping only
'/', so on Windows an entry named with '\' separators kept its
directory components and was extracted wherever they pointed. Strip
both separators, and reject any entry whose name still escapes the
extraction folder.

The preset name from the JSON and the bundle id from
bundle_structure.json were joined onto the preset directory unchecked
as well, which let either of them write outside it on every platform.
Both are now validated before anything is written.

The check is the is_path_within_root helper the 3MF importer already
had, moved to Utils so both importers share it. It treats '/' and '\'
as separators on every platform, so a bundle that would escape on one
OS is rejected on all of them.
2026-09-09 15:35:21 +08:00
HanifKohandraistlin7447 4deadc9dce Make Tree-Support Deterministic (#15565)
* Make tree support deterministic without giving up its parallelism

* Break equal-distance ties in the tree support MST by coordinates

* test: cover the determinism this PR fixes

The MST unit tests here cover the tie-break, but the drop_nodes rework
has no test.

Adds two cases to the tree support suite. The thread-scheduling one
slices five configs twice each and compares the support point sequence,
which is what the node ordering moves. The MST tie one pins the branch
diameter and line width that carry Prim's equal-distance ties into the
toolpaths.

slice_with_tree_support takes an optional config list so the second case
can add the tree parameters it needs, and the double-slice comparison is
shared rather than written twice.

Both fail on main without this PR. The first passes from 60d1ceb580, the
second from e148865dd6.

---------

Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-09-09 12:33:42 +08:00
HanifKoh 58bf267fdd Scale Min Junction Width to Prevent Fuzzy Skin From Failing Slice (#15566)
* Fix fuzzy skin failing the slice: the minimum junction width was unscaled

* Unit Tests For Fuzzy Fix

* Cover ridged multifractal noise in the fuzzy skin width floor test

Its output is not bounded to [-1, 1], so it scales past the configured
thickness and drives the junction width negative. The floor has to hold
for any noise value, not just an in-range one.
2026-09-08 16:57:33 +08:00
HanifKoh 8e064659ad Fix Non-Deterministic Slicing - Order Per-Layer Intersection Lines Canonically (#15563)
Fix nondeterministic slicing: order per-layer intersection lines canonically

Facet processing in slice_make_lines() is parallel, so the per-layer line
order depended on thread scheduling. make_loops() consumes that order for
island order and loop start vertices, so the same model could slice to
different G-code run to run.

Sort each layer's lines by a topology-based key. edge_type and flags are
appended to the key purely to break ties: two lines can share every id and
endpoint (a Horizontal facet can emit such a pair) and std::sort is not
stable, so without them that pair's order would stay thread-dependent.
2026-09-08 16:33:12 +08:00
HanifKoh c61d2fe5d1 Fix the Folgertech i3 0.6 nozzle printable area (#15577)
The bed was declared as 0x0, 20x0, 200x200, 0x200 - a triangle - where the
0.4 nozzle profile and the printer have the 200 x 200 square. Found by the
profile validator once it placed the prime tower beside the test cube:
no tower fits inside that outline.
2026-09-08 11:11:54 +08:00
HanifKoh 886d43d37a Fix Support Fill Cost Thresholds Being Frozen By The First Call (#15564)
Fix support fill cost thresholds being frozen by the first call
2026-09-07 16:35:36 +08:00
HanifKoh 1170b048e8 [CLI]: Fix Plate Config Reading and BuildVolume Height Checks (#15479)
* Fix incorrect early exit for CLI mode no-support preventing parameters from being read

* Use PartPlate's m_height to allow CLI to perform proper BuildVolume check

* Add safeguard against extruder_pintable_heights and extruder_areas vector size mismatch

* Preserve printable_height precision in PartPlate/PartPlateList

* Fixed multiple BuildVolume warning issue, and keep check_outside diff minimal
2026-09-04 23:24:59 +08:00
HanifKoh df30e22427 [CLI]: CLI Crash Guards (#15477)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

Part 1 of 3 of the CLI-mode bug sweep, split out of #15452 per review
feedback there. This PR contains the crash fixes.

## Fixes

- **`--outputdir`/`--datadir` with a missing parent directory aborted**
via unguarded `create_directory`. Directories are now created
recursively, with a graceful early exit and a specific error message if
creation fails.
- **`--slice` + `--export-3mf` segfaulted on a from-scratch slice**:
`ConfigOptionVector::get_at()` on an empty vector is `.front()` of an
empty vector (UB). Guards added for `filament_color`/`filament_id` at
the CLI call site, and inside `DynamicPrintConfig::get_filament_type`
for `filament_type`/`filament_is_support`/`filament_id`. Only *empty*
vectors are treated as missing — the existing clamp-to-front behavior
for merely out-of-range indices is preserved, so GUI callers are
unaffected.
- **OOB heap write from stale `filament_self_index` on
`--load-filaments`** (fixes #14181): a 3MF carrying more
`filament_self_index` entries than loaded filaments wrote past the end
of `old_variant_counts`. The guard validates both bounds — entries `>
filament_count` *and* non-positive entries (`< 1`), since a single `0`
in an otherwise-valid array indexes `old_variant_counts[-1]`.
- **Wrong printable-area check** for non-rectangular beds: use the
printable area's bounding box instead of a naive vertex calculation
(fixes #15363).
- **`nozzle_height` and `align_center` were not read into the arrange
config** in CLI mode.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
- Repro'd each crash on CLI before the fix; all resolved after.
- `tests/libslic3r` suite passes; full binary builds clean on Linux.
- Added `get_filament_type` unit tests 

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-04 15:46:03 +08:00
HanifKoh 57ce18d70d [CLI]: CLI Argument Parsing Fixes (#15478)
* Reject invalid CLI argument values instead of silently accepting them

* Add read_cli accept/reject tests

* Update Option Type for LogFile argument

* Add read_cli vector option tests

* Accept common bool spellings on the CLI, cover --logfile in tests

* Add unit tests for truthy bool parsing
2026-09-04 11:31:42 +08:00