The grey header strip carrying the Settings/Old Value/New Value titles was
added to the tab panel without wxEXPAND, so it stayed at the width its own
sizer fitted it to. The panel, the scrolled window and every category and
value row below it are added with wxEXPAND and do follow the dialog width,
so as soon as the dialog came out wider than the table, the header stopped
short and left a bare strip of panel to the right of New Value.
The dialog takes its width from the widest child of the main sizer, which
includes the button row. In the Transfer/Discard/Save variant three Choice
buttons, each with a hard FromDIP(100) minimum, plus the gaps, the checkbox
and the margins come within a few pixels of the table width, and at higher
display scaling they overrun it.
Give the header the panel width and send the surplus to a stretch spacer
after the last column, so the columns stay put: they are hard minimum sizes
on both sides, and the rows below leave that same area empty, so header and
values remain aligned. The Settings column drops to proportion 0 so it does
not absorb the surplus and slide the other two titles out of alignment.
The header is unchanged when the dialog is not wider than its table.
Verified on Linux with a release build: with the dialog widened, the header
goes from ending at x=511 to spanning the full 749, and the pixel difference
between the two builds is confined to the strip that was previously bare. At
normal width the two builds render identically. Not verified on Windows.
# Description
Adds a sketch-first parametric CAD tab to the slicer: sketch → constrain
→ solid features
→ commit to plate. The feature recipe is persisted inside the 3MF, so
reopening a project
restores an editable model rather than a frozen mesh.
Opening this at @SoftFever's request, so the code is easier to read than
a fork.
**The number worth reading first:** the diff is large, but almost all of
it is new files.
Existing upstream code is touched in **23 files, +622 / -88 total**.
That is the entire
negotiable surface. The largest single one is `GLCanvas3D.cpp` at
+149/-14 (a pick path for
the CAD viewport); everything else is under 60 lines.
One thing about the raw diff: the file count includes everything new,
and the negotiable
surface is the 23 modified files above. Thanks for merging `main` in —
the branch is current
again, and I have kept building on top of it.
The regenerated i18n catalogues (`OrcaSlicer.pot`, `OrcaSlicer_it.po`,
`list.txt`) have been
kept OUT of this branch deliberately — they were 27,314 added lines of
build product standing
between you and the code. They regenerate from source with
`scripts/run_gettext.sh` whenever
you want them refreshed. A Romanian catalogue that had been riding along
was pulled out at
the same time — a translation has no business being reviewed inside a
CAD feature PR.
| | |
|---|---|
| Kernel | OCCT — already linked for STEP import. The dependency delta
is one line: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`. Measured cost
in
[`docs/cad_dependency_weight.md`](https://github.com/tommasobbianchi/Orca-Cad/blob/cad-mainline/docs/CAD/cad_dependency_weight.md)
|
| Constraint solver | vendored SolveSpace `libslvs` subset, 21 files /
~10k lines under `src/libslic3r/slvs/` |
| Build gate | `SLIC3R_CAD` (default ON). With it OFF the tab is not
compiled and the deps prefix matches upstream exactly |
| Persistence | CAD recipe embedded in both the 3MF and BBS-3MF writers
|
| User docs |
[`docs/design_tab.md`](https://github.com/tommasobbianchi/Orca-Cad/blob/cad-mainline/docs/CAD/design_tab.md)
|
| Interaction model | object-driven — point at geometry, it offers the
verbs that apply:
[`docs/cad_ux_guidelines.md`](https://github.com/tommasobbianchi/Orca-Cad/blob/cad-mainline/docs/CAD/cad_ux_guidelines.md)
|
### Why it belongs in the slicer
Every round trip through an external CAD tool costs an export, a
re-import, and the design
intent both steps discard. A part changed after slicing should come back
to its feature
history, not to a mesh. Keeping the model in the slicer preserves that
loop — nozzle
diameter, build volume and material are known at design time. Longer
argument in
[`docs/design_tab_upstream_portability.md`](https://github.com/tommasobbianchi/Orca-Cad/blob/cad-mainline/docs/CAD/design_tab_upstream_portability.md).
### Two things I'd rather you hear from me than find
**Licensing.** The vendored solver is **GPL-3.0**, not LGPL
(`src/libslic3r/slvs/LICENSE`).
The combined work is distributable under AGPL-3.0 and the compatibility
argument is written
out in the portability doc, but this is a project-level decision and I
would like it
confirmed explicitly rather than assumed. If GPL-3.0 in-tree is not
acceptable, the solver
is the separable part — the timeline, features and persistence do not
depend on it.
**One CMake change is larger than it looks.** `CMakeLists.txt` is
+41/-56: it replaces a
hand-maintained list of OCCT DLLs to copy on Windows with a glob plus an
assertion that
every linked toolkit actually has a DLL. The explicit list had already
drifted from what
`libslic3r` links and shipped a portable that died at launch with `error
126`. Happy to
split that out into its own PR if you'd prefer it reviewed separately.
### Not verified
- No automated GUI test. A green kernel run says nothing about the
viewport — synthetic
clicks never drift, so the suite and the UI are two separate realities.
- Card wiring for 9 of the 16 late-wired tools has never been
click-tested.
- The click-test defect rate has not converged: one pass found nothing,
four further days
of work found five more defects. I would not present the quiet pass as
evidence of
stability.
# Screenshots/Recordings/Graphs
One part, start to finish: sketch it, feature it, print it — without
leaving the slicer.

**1. Sketch, constrained and dimensioned.** A 100 × 90 rounded rectangle
drawn straight onto
the bed, R20 corners, live dimensions, and the solver's remaining
degrees of freedom reported
in the panel. The bed is the sketch plane, so the part is sized against
the machine it will be
printed on from the first line.

**2. The feature tree is the part.** `Sketch1 → Extrude2 → Chamfer3 →
Sketch4 → Extrude5 →
Hole6 → Thread7`. Every step stays editable and re-evaluates downstream
— the modelled thread
in the boss is a real helical feature, not a texture.

**3. Committed to the plate.** The same body arrives in Prepare as
`Design Body`,
100 × 90 × 78 mm, 581,634 mm³, ready for a Sovol Zero and PETG. No
export, no re-import, no
lost design intent.

**4. Sliced.** The thread comes out as real helical toolpaths, and the
estimate is 3h26m /
134.54 g. This is the whole argument for the feature in one frame: the
geometry that was
parametric two screens ago is now G-code, and it is still parametric if
you go back.
## Tests
215 `TEST_CASE` blocks across 6 new test files, plus 2 `SCENARIO`s added
to
`tests/libslic3r/test_3mf.cpp` covering the CAD recipe's round trip
through both 3MF
writers. `scripts/kernel-test.sh` is the headless contract: it builds
only
`libslic3r_tests`, needs no display, and exit 0 means the CAD suite
passed.
Happy to slice this differently — kernel + solver first, GUI second — if
that reviews
better for you.
Merged by /bot merge on behalf of @JAYO3D-Official (id 320896770).
Grants: resources/profiles/OrcaFilamentLibrary/filament/JAYO, resources/profiles/OrcaFilamentLibrary.json
Head: fb9a6d70a0
* CLI: --inspect-paint — dump per-facet paint state as JSON
Reads the per-facet enforcer/blocker/extruder/fuzzy-skin state stored
on every ModelVolume (supported_facets / seam_facets /
mmu_segmentation_facets / fuzzy_skin_facets) and emits a structured
JSON summary to stdout. Machine-readable alternative to opening the
paint gizmos.
Per (object, volume, layer, state): facet count, surface area in
mm², and mesh-local bounding box. Empty layers collapse to
{"empty": true}. Summary at the top level rolls up totals.
One correctness detail worth calling out: FacetsAnnotation::
get_facets_strict returns an indexed_triangle_set whose `vertices`
array is the whole source mesh — only `indices` are filtered to the
painted triangles. A naive bounding_box(its) would report the whole
mesh's bbox even when only a few facets are painted. The helper
its_referenced_bbox() walks only the vertices actually indexed by
the painted triangles, so `bbox` correctly localizes the painted
region.
Rationale: every paint-driven workflow — GUI-painted .3mf verified
in CI, AI agents planning support enforcers, MMU color layout checks
— needs to know what's already painted on a model. Today that's a
GUI-only read. --inspect-paint closes that loop for scripted callers.
New file src/slic3r/Utils/PaintCLI.{hpp,cpp} (~215 lines). Depends
only on Model, TriangleMesh, TriangleSelector, FacetsAnnotation, and
nlohmann::json — all already in tree. No new dependencies, no
signature changes, no behavior change when the flag is absent.
Registered as an action (parallel to --info) so it satisfies the
"needs an action" check and bypasses the GUI fallback; control falls
through the normal post-action path to a clean exit 0.
Verification:
unpainted STL: every layer {"empty": true}, summary zero
GUI-painted .3mf: enforcer count / area / bbox match painter
clean JSON: parseable via jq
* CLI --inspect-paint: exit after printing, reject conflicting actions
- Finish like the end of CLI::run once the JSON is written, as the
tooltip says. The callback manager is Linux-only, so its use is
guarded.
- Reject actions that would otherwise be skipped without notice
(--slice, --export-3mf, ...) before loading. Load-time options such as
--uptodate are still accepted.
- Replace invalid UTF-8 in object names and paths instead of throwing.
- Report every input file as sources; inputs are merged into one model
before actions run.
* CLI --inspect-paint: reject a run without input
Without an input file or --load-assemble-list there is nothing to
inspect, and the run printed nothing and exited 0. Reject it up front
with CLI_INVALID_PARAMS, next to the other invalid-parameter checks.
The deps superbuild passes the Visual Studio generator and platform to
every sub-build but not the toolset, so build_win.bat -d -l without -x
compiled every dependency with cl even though the superbuild had been
configured with -T ClangCL; CMake replaces the forwarded
CMAKE_<LANG>_COMPILER with whatever the toolset ran. The recipes that
adapt to clang-cl then disagreed with what had been built, and
wxInspector told FindwxWidgets to look in lib/clang_x64_lib while the
cl-built wxWidgets had installed into lib/vc_x64_lib:
Could NOT find wxWidgets (missing: wxWidgets_LIBRARIES
wxWidgets_INCLUDE_DIRS core base aui propgrid)
Forward CMAKE_GENERATOR_TOOLSET as well, so the dependencies compile
with clang-cl under MSBuild the way they already do under Ninja. Four
of them need more than that:
- OpenSSL always builds with cl, and MSBuild runs its nmake steps in
the project's toolset environment, where ClangCL puts clang's include
directory first and cl trips over clang's stdint.h. The project gets
the default toolset.
- Boost.Container's dlmalloc needs -Wno-incompatible-pointer-types
under clang. boost_container links as C++, and the Visual Studio
generator writes only the link language's flags into the project, so
its C file never saw CMAKE_C_FLAGS. Under that generator the option
goes through the C++ flags as well, with the defaults kept.
- Draco's tools and NLopt's testopt compile sources their own static
library also contains. MSBuild lists libraries before objects and
lld-link resolves archive members as each input arrives, so the
library's copy is pulled in before the executable's own object and
the link fails on duplicate symbols; link.exe defers the search and
Ninja lists the objects first. Nothing uses those executables, so
they get /FORCE:MULTIPLE there.
The Ninja path is unchanged: the generated configure commands of all
29 dependencies are identical before and after. OCCT's arm64 override
to cl still applies under Ninja but not under the Visual Studio
generator, where the toolset wins; that combination never built and is
left for a follow-up.
* Toolchange Cyclic Order
* Apply cyclic order to first layer
* Unit test
* Copilot fixes
---------
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
* ci: build Windows with build_win.bat and drop the old scripts
The deps and slicer jobs called build_release_vs.bat; they now call
build_win.bat. --deps-dir and --build-dir name the build/build-arm64
directories the cache keys and later steps already use, and the
script's own VsDevCmd call replaces the Enter-VsDevShell blocks.
With both stages configured each way into the same directory, the deps
superbuild is byte-identical and the slicer build files are
byte-identical apart from CMakeCache.txt recording how DEP_BUILD_DIR
was set.
Two changes beyond the script swap:
- The compiler is the clang-cl bundled with Visual Studio, the script's
default. The old script's bare "clang-cl" resolved to the LLVM on the
runner image's PATH, 20.1.8 on x64 and 22.1.8 on arm64; both arches
now build with the 22.1.3 VS 18.9 ships. Cached dependencies are only
rebuilt when deps/ changes, so they stay on the LLVM they were built
with; the arm64 leg already links deps built with Clang 19 into a
Clang 22 slicer.
- The deps job no longer zips the dependencies afterwards. The zip was
never uploaded and was not in the cached path.
A failed cmake --build now fails the job. The old script returned 0, so
the arm64 failure fixed in #15719 was reported as success and the
half-built dependencies were saved to the cache.
build_release.bat, build_release_vs.bat and build_release_vs2022.bat are
removed; nothing referenced them any more.
* ci: run Build all when the Windows build script changes; tests doc builds the deps
The push filter of build_all.yml never listed a build script, and CI
now depends on build_win.bat, so it and its test suite join the list
the pull_request filter already has.
tests/AGENTS.md told Windows to run build_win.bat --run-tests, which
only implies -s and stops at the dependency check on a clean checkout.
The old build_release_vs.bat tests built the dependencies first, so
the line now says -ds --run-tests.
* Run every profile maintenance job from one tool
orca_id_tool.py becomes orca_profile_tool.py, and orca_extra_profile_check.py
and orca_filament_lib.py fold into it as subcommands: check, generate-id, fix,
trim, update-index and update-snapshot. The three scripts already overlapped --
the checker imported half of its rules from the id tool, which in turn kept a
copy-pasted set of output helpers to avoid the resulting import cycle -- while
disagreeing on how a vendor is enumerated, how a JSON file is read and what the
exit code means. One file settles all three.
check, normalize, trim and update-index reproduce their predecessors exactly; normalize and
update-index were diffed byte-for-byte against the old scripts over a copy of
the whole tree. Deliberate changes: the compatible-printers check no longer
switches itself off when --check-materials is passed, an error exits 1 rather
than -1, update-index honours --profile-type and reports a profile it cannot
place instead of dropping it from the index, fix and update-index gained
--dry-run, trim keeps an unindexed file some surviving profile still inherits
from, and vendors are enumerated as directories with an index -- which is why
blacklist.json, a data file that an unscoped index rebuild once wrote four empty
list sections into, loses them here and will not collect them again. The dead
rename_filament_system() helper is gone.
The suite under scripts/tests now covers the maintenance commands too, and CI
runs it; nothing there ran in CI before. No shipped profile data changes apart
from those four keys.
* update vendor index files with "python3 ./scripts/orca_profile_tool.py update-index" and "python3 ./scripts/orca_profile_tool.py normalize"
* CLI: --ground-face-* / --lay-flat / --center-on-bed orientation primitives
Adds the CLI counterparts to the GUI's lay-flat / face-pick gizmos.
Scripted / CI / AI pipelines can now set orientation without rendering
a wxWidgets frame; today the only way is a GUI round-trip.
New CLI actions (all operate in the mesh-local frame so they compose
with prior --rotate-* / --orient flags):
--ground-largest-face 1 Auto-detect the largest planar-face
or --lay-flat 1 cluster (area-weighted), rotate so its
normal points -Z. Covers "this part has
one obvious flat side" cases.
--ground-face-normal NX,NY,NZ Pick the face whose mesh-local
normal best matches the given
vector; ground it. e.g.
`--ground-face-normal 1,0,0`
stands a part on its +X side.
--ground-face-point X,Y,Z Find the triangle containing the
given mesh-local point; ground its
face. Disambiguates when several
faces share a normal (largest
containing triangle wins).
--center-on-bed 1 Translate so the XY bounding-box
centroid lands at the bed center
(derived from printable_area).
New file `src/slic3r/Utils/MeshOrient.{hpp,cpp}`:
- collect_triangles_object / compute_face_clusters — quantize
per-triangle normals (0.001, ~0.06°) and area-weighted-average
within clusters. Same clustering logic used by lay-flat.
- apply_ground_rotation — same math as Selection::flattening_rotate
in the GUI (Selection.cpp:1432): world-space quaternion from the
transformed normal to -Z, applied as offset * new_rot * old_no_offset
on every instance of every object, then a per-instance Z-lift so the
grounded face lands at exactly 0 (avoids "No layers were detected"
from FP-error z≈-1e-9).
- ground_face_point uses a top-N cluster search + point-in-triangle
test in local space; largest-area triangle wins on ambiguity.
Rationale: without these, any CLI pipeline that needs a specific
face on the bed must either encode custom rotation math per part or
break out of the pipeline into the GUI. Both are bad for
reproducibility. The --ground-face-* triple + the largest-face
auto-mode cover essentially every orientation intent expressible
in a slicing wizard.
Scope:
- `src/slic3r/Utils/MeshOrient.{hpp,cpp}` — new, ~420 lines
- `src/slic3r/CMakeLists.txt` — 2-line registration
- `src/libslic3r/PrintConfig.cpp` — 5 new CLIMiscConfigDef entries
- `src/OrcaSlicer.cpp` — 58-line handler block + 1 include
No behaviour change when the flags are absent.
(cherry picked from commit c45a9795e1)
* CLI grounding: choose among the Lay on Face planes, per object
Addresses review:
- Move the geometry of GLGizmoFlatten::update_planes() into
libslic3r/LayOnFace and use it from the gizmo and the CLI, so the
--ground-* options pick convex-hull faces per object and instance,
with part transformations (--rotate-x/y) applied.
- Drop --center-on-bed, the --lay-flat alias and MeshOrient; make
--ground-largest-face a coBool.
- Parse --ground-face-normal and --ground-face-point strictly. A point
that only some objects contain grounds those and leaves the others.
- Fold in --inspect-mesh from #14603, reporting the same planes.
- Tests in tests/libslic3r/test_lay_on_face.cpp: bounding boxes before
and after, rotate then ground, two objects, and a ribbed part whose
parallel inner faces outsum its base.
* CLI --inspect-mesh, --ground-face-*: reject missing input and empty values
- Without an input file or --load-assemble-list, --inspect-mesh printed
nothing and exited 0. Reject it up front with CLI_INVALID_PARAMS.
- An explicit empty --ground-face-normal or --ground-face-point was
silently ignored. Only options given on the command line reach the
transforms loop, so an empty value now fails the strict parse like any
other malformed value.
# Description
Add `--strict` for CI and scripted pipelines, and a structured
`warnings`
array in `result.json`.
## `--strict`
A NON_CRITICAL slicing warning is logged and the slice succeeds: return
code
`0`, G-code written. That suits interactive use, but a pipeline then
ships a
slice with a warning nobody saw. With `--strict`, such a warning fails
the run
with `CLI_SLICING_ERROR` before the G-code is exported. Without the
flag,
nothing changes.
In FFF the warning that reaches this path is "support needed but
disabled"
(`PrintObject::generate_support_material`). `--no-check` skips that
check, so
`--strict --no-check` is rejected with `CLI_INVALID_PARAMS`.
`--strict` is read before any work, so it doesn't depend on argument
order and
`result.json` reports it for early failures as well.
## `result.json`
Two new top-level fields:
- `warnings`: `[{"class", ...details}]`. One class is wired:
`slicing_warning_non_critical` with `plate_id` and `text`, recorded
whenever
such a warning fires, with or without `--strict`. The array also fills
on
runs that succeed, so `return_code` stays the verdict.
- `strict_mode`: whether `--strict` was on.
`record_exit_reson` writes `result.json` on Linux only, so both fields
exist
only there. The non-zero exit works on every platform.
## Tests
- `tests/fff_print/test_support_material.cpp` (all platforms): an
overhang
sliced with support off raises the NON_CRITICAL support-needed status,
and
the no-check flag suppresses it.
- `tests/cli/test_cli_strict.sh` (Linux only): runs `orca-slicer`
without
flags, with `--strict`, and with `--strict --no-check`, and checks the
shell
status and `result.json` of each. It runs the built binary, so it
carries the
`RequiresApp` label, which `scripts/run_unit_tests.sh` excludes because
the
unit-test job only receives `build/tests`. Run it with
`ctest --test-dir build/tests -C Release -L RequiresApp`.
- CI: `unit_tests.yml` now passes `Release` on Linux too.
`build_linux.sh`
configures Ninja Multi-Config, and without a config ctest drops the
labels of
plain `add_test()` tests, so this test ran as "Not Run" instead of being
excluded. The docs that assumed Linux was single-config are corrected
too.
Built and run locally on Linux (GCC 14) on current `main`: both tests
pass,
and the touched files compile clean under Clang with `-Werror`.
* ci(flatpak): run the unit suite in a separate job, mirroring the other arches
Alternative to the in-job step: split build and test like the Linux/Windows/
macOS legs. The flatpak build now builds the test binaries in-sandbox (the
action's run-tests fires the module's build-only test-commands), prunes the
kept build tree to the test binaries + CTest metadata + data, and uploads it
with /app as a test asset (size reported to the run summary).
A new unit_tests_flatpak matrix job downloads that asset on a native runner,
restores the module-build symlink, and runs the suite via flatpak-builder
--run (which bind-mounts /run/build so TEST_DATA_DIR resolves) against the
GNOME SDK's bounds-checked STL. Results feed publish_test_results.
Costs a per-arch asset upload/download + a runtime install on the test
runner; the trade-off vs the in-job step is a genuine separate graph box.
* ci(flatpak): run tests via `flatpak build` to avoid rofiles-fuse
`flatpak-builder --run` sets up a rofiles-fuse overlay that this CI container
rejects (Failure spawning rofiles-fuse, exit_status: 256), even in a fresh job
with a machine-id and the runtime installed, and --disable-rofiles-fuse is not
accepted in --run mode. `flatpak build` enters the sandbox via bwrap directly,
so it sidesteps rofiles-fuse; bind-mounting the build tree at /run/build gives
the same path the compiled-in TEST_DATA_DIR expects.
* ci(flatpak): slim the test asset (strip binaries, drop source tree)
The first cut shipped ~1 GB: the test exes carried debug info (the SDK builds
with -g and only the app gets stripped) and the packaged module dir included
the whole copied source tree the tests never read at runtime. Strip the test
binaries and keep only build_flatpak/tests, tests/ (TEST_DATA_DIR) and scripts/.
The irreducible remainder is /app, which the exes link against.
* ci(flatpak): extract the test run into a reusable unit_tests_flatpak workflow
Move the flatpak test job out of build_all.yml into a reusable
unit_tests_flatpak.yml, called once per arch (Flatpak x86_64 / aarch64) the
same way the other arches call unit_tests.yml. build_all.yml keeps only the
build + asset packaging; the reusable workflow downloads the asset, runs the
suite via `flatpak build`, and uploads results as test-results-<artifact> for
publish_test_results. Drops the now-unused manifest checkout (flatpak build
does not need it).
* ci(flatpak): trim comments to the non-obvious
No behavior change.
* ci(flatpak): drop redundant caller comment
* ci(flatpak): drop redundant trim comment
* ci(flatpak): drop size-report scaffolding and redundant if-guards
* ci(flatpak): force the app module to rebuild so the test asset always exists
flatpak-builder caches modules by content hash and skips a hit, producing no
build tree and no test asset, so a re-run of the same commit would leave the
separate test job with nothing to download. Inject a per-run cache-buster into
the OrcaSlicer module's build-options (part of its cache key) so it always
rebuilds, mirroring how the other arches cache only deps and always rebuild the
app and tests. The deps modules stay cached.
* ci(flatpak): trim cache-buster comment, fix stale step name
* fix: guard H2C per-filament array reads against short config arrays
The H2C tool-ordering, wipe-tower, and g-code export paths index per-filament
config arrays by filament/tool id. A config with fewer entries than the filament
count (partial or legacy projects, minimal test configs) makes these reads run
past the end of the vector: silent under a normal STL, but UB that aborts under
the flatpak build's bounds-checked STL (_GLIBCXX_ASSERTIONS).
Route the reads through the existing clamping accessors (get_at,
get_filament_category, is_in_same_extruder) and add a small clamp helper for
filament_change_length. The guards are no-ops when the arrays are sized to the
filament count, so correctly specified configs are unaffected.
* ci(flatpak): build filament_group_tests too
The suite landed on main after this branch was cut and arrived via a later merge,
so it was missing from the target list and ctest failed the leg with
filament_group_tests_NOT_BUILT.
Not tests/all, which build_linux.sh uses: that is a Ninja subdirectory target and
this build configures with the default Makefile generator, where it does not exist.
* ci(flatpak): give the embedded-interpreter tests a valid Python home
python_test_support.hpp sets PyConfig.home to <testdir>/python when that
path resolves. WIN32/APPLE populate it with a copied bundled runtime; the
flatpak leg had no such branch, so home resolved to a directory with no
stdlib and all 21 embedded plugin tests failed at "failed to get the
Python codec of the filesystem encoding".
Symlink <testdir>/python to the bundled /app/libpython that already ships
in the flatpak (the test exe links libpython3.12.so from there via rpath),
so the interpreter initializes without duplicating the runtime.
* ci(flatpak): sync the ToolOrdering guard mirror with #14789
Match #14709's build_filament_group_context guard to the version on
#14789 (size filament_info to filament_nums, truncate filament_ids)
so the folded guard is a byte-identical mirror that drops cleanly when
#14789 merges, instead of leaving a stale hunk that conflicts on rebase.
* fix: guard WipeTower per-filament array reads against short config arrays
The BambuStudio WipeTower sync reintroduced raw per-filament array
indexing that reads out of bounds when a config leaves an array shorter
than the filament count: m_physical_extruder_map in format_line_M104/M109
(indexed even when empty), and m_filament_categories in get_wall_skip_points
and get_wall_filament_for_all_layer. Silent on a normal STL, a hard abort
under the bounds-checked STL the Flatpak build uses.
Bounds-check the physical extruder map before indexing (omitting the T
token, as the existing -1 path already does), and route the two raw
m_filament_categories reads through the clamping get_filament_category()
accessor the surrounding code already uses. No change for correctly-sized
configs.
* fix: default-initialize WallToolPathsParams fields
min_length_factor and is_top_or_bottom_layer had no default initializers, and the FillConcentric/FillConcentricInternal callers never set them, so WallToolPaths::removeSmallLines() thresholded on stack garbage. Which short extrusion lines it dropped then depended on memory layout, so concentric solid-infill output was nondeterministic between runs and across machines. Give every member a default, matching the adjacent FillParams. The perimeter path was already fine because it builds the struct via make_paths_params().
* fix: bounds-check the toolchange flush-volume and HRC per-filament lookups
GCode::set_extruder's toolchange flush-volume lookup and
GCodeProcessor::update_slice_warnings's HRC check index per-filament and
per-extruder arrays (flush_volumes_matrix, the filament map, the nozzle list)
by filament/extruder id. When a config leaves one of those arrays shorter than
the filament count (partial or legacy multi-extruder projects, minimal
configs), the reads run off the end: silent on a normal STL, a hard abort under
_GLIBCXX_ASSERTIONS.
Route both reads through bounds checks: the flush lookup falls back to no flush,
matching the existing unknown-old-filament branch beside it, and the HRC check
skips an unmapped filament, mirroring the required_nozzle_HRC guard on the line
above. When the arrays are sized to the filament count the values are unchanged,
so correctly-specified configs are unaffected.
* ci: retrigger checks
* ci: name the flatpak rebuild token after the cache it defeats
Since #15650 the Flatpak job also has a compiler cache, so a bare
"cache-buster" no longer says which cache is meant. Call it
flatpak_builder_cache_buster, and name the build-dir trim step after
the flatpak-builder cache save it keeps lean.
* ci: ship resources/profiles and resources/printers in the flatpak test asset
Two slic3rutils tests added in 4aa0e1d60b read
resources/printers/bambu_filament_ids.json through PROFILES_DIR/.., and
the asset dropped resources/ entirely, so both failed parsing an empty
stream on each Flatpak leg. Keep the two subtrees the tests reach;
test_gcodewriter's shipped-profile case stops skipping on this leg too.
* ci: restore the CRLF line endings of build_all.yml
The last merge from upstream/main rewrote the file with LF endings, which
turns the 60-line change into a whole-file diff on GitHub. Upstream has had
this file as CRLF since it was created, so put it back.
* ci: trigger Build all on changes to the unit-test workflows
The path filters only matched build_*.yml, so an edit to unit_tests.yml or
unit_tests_flatpak.yml could merge without ever running.
* ci: put a timeout on the flatpak unit-test step
Matches the 20 minutes of the regular unit-test workflow; without it a hung
test holds the runner for the six-hour job default.
VS 2026's ARM64 code generator needs about 27 GB for
_PyUnicode_ToNumeric, a switch with 1951 cases in
Objects/unicodetype_db.h; the same file takes under 1 GB on x64. The
16 GB CI runner has an 18.9 GB commit limit and only gets through when
Windows grows the pagefile on the temp disk in time, so cold arm64
dependency builds fail at random with C1002 "compiler is out of heap
space". build_release_vs.bat returns 0 on failure, so the job still
reports success and the incomplete dependencies are cached.
A property sheet compiles that one file with optimisation off on arm64;
the rest stays whole-program optimised and x64 is unchanged.
MSBuild reads it from PCbuild/msbuild.rsp, which is now written at
configure time and copied in, so a checkout path with spaces works too.
* fix: bounds-check the toolchange flush-volume and HRC per-filament lookups
GCode::set_extruder's toolchange flush-volume lookup and
GCodeProcessor::update_slice_warnings's HRC check index per-filament and
per-extruder arrays (flush_volumes_matrix, the filament map, the nozzle list)
by filament/extruder id. When a config leaves one of those arrays shorter than
the filament count (partial or legacy multi-extruder projects, minimal
configs), the reads run off the end: silent on a normal STL, a hard abort under
_GLIBCXX_ASSERTIONS.
Route both reads through bounds checks: the flush lookup falls back to no flush,
matching the existing unknown-old-filament branch beside it, and the HRC check
skips an unmapped filament, mirroring the required_nozzle_HRC guard on the line
above. When the arrays are sized to the filament count the values are unchanged,
so correctly-specified configs are unaffected.
* ci: retrigger checks
* fix: guard H2C per-filament array reads against short config arrays
The H2C tool-ordering, wipe-tower, and g-code export paths index per-filament
config arrays by filament/tool id. A config with fewer entries than the filament
count (partial or legacy projects, minimal test configs) makes these reads run
past the end of the vector: silent under a normal STL, but UB that aborts under
the flatpak build's bounds-checked STL (_GLIBCXX_ASSERTIONS).
Route the reads through the existing clamping accessors (get_at,
get_filament_category, is_in_same_extruder) and add a small clamp helper for
filament_change_length. The guards are no-ops when the arrays are sized to the
filament count, so correctly specified configs are unaffected.
* fix: size the grouping context's filament_info to the filament count
build_filament_group_context built model_info.filament_info by walking
filament_type, so a config whose filament_type is shorter than the filament
count produced a short vector. FilamentGroup indexes filament_info by filament
id, so clamping the individual reads only moved the out-of-bounds access
downstream. Loop to filament_nums and read all three fields through get_at,
and drop filament_ids entries past the filament count, since the grouping code
pairs filament_ids and filament_info by position.
Adds a regression test with four filaments and one-entry filament_type /
filament_is_support. Without the fix it throws bad_alloc from copying a garbage
std::string read past the end.
* fix: guard the carousel nozzle-change length reads too
The carousel branch added in b90ac13d86/b0dddb4648 reads
m_filaments_change_length by tool id without a bounds check, the same
pattern this branch already routed through filament_change_length_at
a few lines above in both plan_toolchange and plan_tower_new.
* fix: guard WipeTower per-filament array reads against short config arrays
The BambuStudio WipeTower sync reintroduced raw per-filament array
indexing that reads out of bounds when a config leaves an array shorter
than the filament count: m_physical_extruder_map in format_line_M104/M109
(indexed even when empty), and m_filament_categories in get_wall_skip_points
and get_wall_filament_for_all_layer. Silent on a normal STL, a hard abort
under the bounds-checked STL the Flatpak build uses.
Bounds-check the physical extruder map before indexing (omitting the T
token, as the existing -1 path already does), and route the two raw
m_filament_categories reads through the clamping get_filament_category()
accessor the surrounding code already uses. No change for correctly-sized
configs.
# 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)
# 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)
The manifest resolver loaded OrcaFilamentLibrary once per vendor it
resolved through, so a run that mixes vendors parsed the library tree
again for each of them. The library is now cached like any other vendor
tree, keyed on its root and substitution rule, and doubles as the base
every vendor under that root loads against. A vendor bundle only reads
from its base while loading, so sharing the instance is safe.
The cache key carries the substitution rule as its enum, and the lookup
lambdas take a const bundle since they only read.
`--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.
Opening a model with a relative path, for example `orca-slicer ./some.3mf`,
failed with "Loading of a model file failed." and "The file does not contain
any geometry data.", while the same file opened by an absolute path or by
drag and drop worked.
GUI_App::init_app_config() changes the working directory to <data_dir>/log,
and it runs from the GUI_App constructor because the app config is needed
early for instance checking. The input files are opened much later, in
post_init(), so a path still relative at that point resolved against the log
directory instead of the directory OrcaSlicer was started from, and the 3MF
reader failed to open it.
Resolve the input paths in CLI::setup(), which runs before GUI_App is
constructed and therefore before the working directory moves. Absolute paths
are returned unchanged, so the forms that open today are unaffected, and
custom open protocol URLs are passed through since post_init() hands those to
the downloader rather than the file loader.
The working directory change is left alone. It was added in #3248 so the TUTK
logs land in the data directory instead of the working directory (#3209).
The U1's heated bed tops out at 100 °C, but these profiles requested
105-110 °C, which leads to print errors unless the user modifies the
printer's firmware configuration.
Affected profiles:
- Snapmaker ABS @U1 base (110/105 → 100)
- Snapmaker ASA @U1 base (110 → 100)
- Fiberon ASA-CF08 @Snapmaker U1 base (105 → 100)
- Fiberon PPS-GF20 @Snapmaker U1 base (105 → 100)
Bumps Snapmaker.json to 02.04.00.10.
Co-authored-by: yw4z <ywsyildiz@gmail.com>
--export-settings already writes the merged config as JSON at the right
point in the CLI flow. Passing - writes the same document to stdout.
- ConfigBase::save_to_json gains a stream overload. The file overload
serializes through it before opening the file, so the format is
unchanged and a config that cannot be serialized leaves an 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.
- - is rejected up front when combined with an action or transform that
can write to stdout or does real work, so stdout carries only the
JSON.
- The unconditional "skip locked instance" stdout write during arrange
now goes to the log.
- Tests in tests/libslic3r/test_config.cpp.
* Extract Layer::choose_ironing_extruder for unit-testable ironing routing
The ironing extruder selection in make_ironing() was a 5-line nested
conditional inlined at the top of the loop, with no isolated test
coverage. Pull the gating into a static helper so the routing decision
is unit-testable without spinning up the slicing pipeline.
Pure refactor: the helper preserves the original logic bit-for-bit
(NoIroning -> -1; AllSolid always enabled; TopSurfaces and TopmostOnly
require some top shells or, in spiral mode, more than one bottom shell;
TopmostOnly additionally requires being on the topmost layer; enabled
ironing routes to solid_infill_filament).
Add tests/fff_print/test_choose_ironing_extruder.cpp covering:
- AllSolid regardless of layer position
- TopSurfaces with top_shell_layers > 0
- TopSurfaces with top_shell_layers=0 + spiral mode + bottom_shell_layers>1
- TopmostOnly + topmost layer
- NoIroning short-circuit
- TopSurfaces with top_shell_layers=0 (and not spiral) -> disabled
- TopSurfaces, spiral, but bottom_shell_layers=1 -> disabled
- TopmostOnly on a non-topmost layer -> disabled
* Move ironing routing test into the Fill subsystem file
Rename the test to tests/libslic3r/test_fill.cpp and tag it [Fill] to
match the subsystem it covers, use flat behavioral test cases with
GENERATE for the parameterized ones, and drop the history narration from
the code comments.
* tests: move ironing routing tests into fff_print/test_fill.cpp
Keeps the Fill tests in one file, alongside the existing ironing
rotation-template test.
Cover the two cache paths the first test left open: a vendor tree that fails to load is retried on the next resolution instead of being served from the cache, and a type-probed filament resolved through resolve_preset_config_type reuses the OrcaFilamentLibrary base already loaded for a sibling.
Resolving a system preset through its vendor manifest loaded the whole vendor tree and the filament library from JSON, and the CLI did that separately for every --load-settings and --load-filaments file. A run with machine, process and filament presets parsed BBL's 2,879 profile files and the library's 512 three times over, about a second each.
Keep the library and vendor bundles loaded by the manifest path on the PresetBundle that resolved them, keyed by source root, vendor and substitution rule, and have the CLI resolve every system preset through one bundle for the whole run. A failed load is not kept, so errors are reported as before.
On a cube slice with X1C machine, process and PLA presets: 2.42 s -> 0.93 s, BBL.json opened once instead of three times, identical G-code.
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).
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.
Runs orca-test-repo's full override-sweep effect stage (two shards) and the GUI-vs-CLI parity harness every night against the latest successful build_all Linux AppImage, with sources checked out at that build's commit. Kept out of the per-build regression step, whose time budget it would exceed, and never gates a build.
# 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 really basic API to push notifications to the plater.
Plugin used in demo:
[plater_notification.py](https://github.com/user-attachments/files/31293074/plater_notification.py)
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
<img width="2172" height="1241" alt="image"
src="https://github.com/user-attachments/assets/540319ca-a11a-4b48-9b80-82fb6b0849d9"
/>
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
* CLI: record command-line overrides in different_settings_to_system
Settings passed on the command line (--sparse-infill-density 25% ...) override
the loaded presets when m_extra_config is applied to m_print_config, but nothing
recorded them in different_settings_to_system. The exported project therefore
carried the new value with no mark that it was modified, and re-opening it in the
GUI reverted it to the system preset's value -- the same failure the preset-leaf
diff fixes for user presets, via a different source of override.
The key set comes from m_config, not m_extra_config. read_cli() puts only what the
user typed into m_config and setup() adds nothing but CLI-own defaults (none of
the keys run() materialises there is a preset option), whereas the CLI writes its
own values into m_extra_config (has_filament_switcher, filament_colour,
filament_map ...), which must not be reported as user overrides.
Values are snapshotted just before the apply and only keys the override actually
changed are recorded: a typed value equal to the loaded one modifies nothing, and
listing it would read as a spurious difference against what the GUI writes. Each
key lands in the column(s) whose preset type owns it -- process, every filament,
printer -- and a key already present is not duplicated. Keys no preset owns
(curr_bed_type, a project setting) land nowhere, as in the GUI.
Follow-up to #15595, split out at review.
* CLI: judge command-line overrides the way the value is read
Review follow-ups on the override recording:
- Lists were compared as whole serialized strings. read_cli() builds a fresh
one-entry list, so --nozzle-temperature 245 against 245,245,245 on a
three-filament project was recorded in every filament column although nothing
changed. Lists are now compared entry by entry with a missing entry read as the
first, as get_at() reads it (and as resize() pads).
- The log line fired for every changed key, including ones no preset owns
(curr_bed_type) and which therefore land in no column. It now fires only when a
column took the key.
- m_print_config.has(key) straight after apply(m_extra_config, true) was always
true, both configs sharing print_config_def; removed. columns.size() >= 2 also
always holds after the resize to filament_count + 2 -- different_settings_to_system
is not a CLI option, so nothing in between can shrink it -- but that rests on code
far away, so it stays a plain check rather than an assert: release builds compile
asserts out, and a _GLIBCXX_ASSERTIONS build would abort on columns[0].
Deliberately NOT done: comparing a key the loaded config lacks against its
built-in default. On reopen the GUI restores an unlisted key from the SYSTEM
preset, not the default. A 3MF written before an option existed leaves it absent
here, so --sparse-infill-density 20% (the default) against a Prusa system 15% would
go unrecorded and be reverted to 15%. Absent keys stay always-recorded:
over-recording is cosmetic, under-recording loses the value. Verified that such a
key really is absent at this point, rather than filled from the system preset.
Reported by HanifKoh and raistlin7447 in review of #15642.
* CLI: evaluate compatible_printers_condition in the compat checks
Slicing from the CLI with --load-settings exits with
CLI_PROCESS_NOT_COMPATIBLE (-17), "The selected printer is not compatible
with the process preset in the 3mf.", for process/printer pairs the GUI
accepts. Reproducible with stock, unmodified Prusa system profiles:
orca-slicer --datadir <datadir> \
--load-settings "<datadir>/system/Prusa/process/0.20mm SPEED @CORE One HF 0.4.json;<datadir>/system/Prusa/machine/Prusa CORE One HF 0.4 nozzle.json" \
--load-filaments "<datadir>/system/Prusa/filament/Prusament PETG @CORE One HF 0.4.json" \
--slice 0 --outputdir /tmp/out model.stl
The four compat checks in CLI::run did a literal name match against the
`compatible_printers` list only:
for (index ...) if (new_print_compatible_printers[index] == new_printer_system_name)
process_compatible = true;
Process profiles that declare compatibility through
`compatible_printers_condition` and leave `compatible_printers` empty are
therefore always reported incompatible -- the condition is never consulted.
For 0.20mm SPEED @CORE One HF 0.4 that condition is:
printer_notes=~/.*PRINTER_MODEL_COREONE[^_a-zA-Z0-9].*/ and
nozzle_diameter[0]==0.4 and printer_notes=~/.*HF_NOZZLE.*/
The GUI does not have this bug: is_compatible_with_printer() in Preset.cpp
treats an empty list as "no explicit constraint" and evaluates the
condition in that case.
Fix: replace the four loops with a check_compat lambda that calls
is_compatible_with_printer() -- the same helper the GUI uses -- wrapping
the already-loaded DynamicPrintConfigs in lightweight Preset /
PresetWithVendorProfile shells. The 3MF-embedded process/printer full
configs are kept in current_process_full_config /
current_printer_full_config so the condition can be evaluated for the
reprocess paths too; those fall back to the previous literal match when
the full config was not preserved.
Behaviour is unchanged where an explicit compatible_printers list exists:
is_compatible_with_printer() performs the same name match, and returns
true when both list and condition are empty, matching the existing
"old 3mf, no compatible printers, set to compatible" path.
Split out of #13731 (section 1) as a standalone, single-purpose change.
Orthogonal to the inherits-chain resolution work in #14718 / #15302 /
#15438; those decide which values a preset resolves to, this decides
whether the resulting pair is considered compatible.
* CLI: translate the 3MF's renamed compatibility keys before the compat check
The 3MF fallback fed the project config to is_compatible_with_printer() as-is,
but a project config does not carry compatible_printers or
compatible_printers_condition. PresetBundle::construct_full_config() erases both
and re-emits them as print_compatible_printers and
compatible_machine_expression_group; they are renamed back only on the
PresetBundle load path, which the CLI does not take. The check therefore saw no
list and no condition, read that as 'no constraint' and accepted every printer.
That is not just a wrong accept. An early true skips the !process_compatible
block that sets machine_switch, so the new printer is never appended to
print_compatible_printers and the exported 3MF stays marked compatible only with
the printer it came from -- which is exactly what that block exists to prevent.
Translate the two keys back before the check. Index 0 of the expression group is
the print preset; the group is filled print, filaments, printer.
Also note in the comment that profiles/BBL/{process,machine}_full/ are gitignored
and generated by nothing in-tree, so current_*_full_config is always empty and
this fallback is the only live path -- not the rare non-BBL case the original
comment implied.
Reported with measurements by HanifKoh in review of #15449.
Preset: add a config-level is_compatible_with_printer() overload
The CLI holds resolved DynamicPrintConfigs, not Presets, so it wrapped them in
throwaway Preset shells at the call site. Moving that into Preset.cpp puts the
compatibility policy -- including the documented fail-open on a malformed
compatible_printers_condition -- in one place for the GUI and the CLI, rather
than leaving a second copy of the plumbing in OrcaSlicer.cpp to drift.
Purely additive: neither existing overload changes, so no GUI behaviour moves.
Requested by HanifKoh in review of #15449.
(cherry picked from commit 14ca1972ef4d3c7d90935d159423013a40a6bd70)
* CLI: never overwrite a real compat key with an empty renamed one
7e7f0e3 translated compatible_machine_expression_group[0] into
compatible_printers_condition whenever the group vector was non-empty. A project
the CLI exported itself carries the real compatible_printers_condition AND an
all-empty group, ["", "", ""], so the valid condition was overwritten with
"", the check saw no constraint, and every printer was accepted.
That fixed GUI-shaped projects and broke CLI-shaped ones. Bisected across six
builds re-slicing one CLI-exported CORE One project with an MK4S: every build
before 7e7f0e3 gives 'compatible 0' and takes the machine-switch path; with it,
'compatible 1' and no switch.
The raw keys now win whenever they carry something; the renamed ones are only a
fallback, and an empty value is never written over a real one. Same for the list:
print_compatible_printers is used only when compatible_printers is absent or
empty and it itself is not.
Found by a peer session re-testing the installed build.
* CLI: record user overrides in different_settings_to_system for 3MF export
Three sites in CLI::run wrote an empty `different_settings_to_system` column
and left a //todo:
//todo: support user machine preset's different settings
different_settings[filament_count+1] = "";
//todo: support system process preset
different_settings[0] = "";
//todo: update different settings of filaments
different_settings[filament_index] = "";
So a 3MF exported by the CLI does not record which keys the user actually
overrode relative to the system parent. Re-opening such a project in the GUI
then shows spurious "unsaved changes", and accepting that dialog can revert
inherited process/filament/machine values to system defaults.
The column could not be filled before because the CLI had no resolved view of
the parent preset. It does now: #15438 builds a PresetBundle for inherits
resolution, so the parent can be looked up by name and diffed against the
resolved leaf. This adds no extra loading -- the bundle is the one already
built, and the helper returns "" whenever it is unavailable or the parent
cannot be found, which is the previous behaviour.
Preset metadata is filtered out of the diff: `inherits`, the three
`*_settings_id` keys, and `compatible_printers` / `compatible_prints` and
their `_condition` variants, which have their own tracking columns
(`inherits_group`, per-slot lists) and would otherwise double-count.
A value already carried by the loaded JSON still wins for the process slot, so
presets saved with a `different_settings_to_system` field behave as before;
the computed value only fills the gap where that field is absent, which is the
case for every user preset in my datadir (0 of 47 carry it).
System presets keep an empty column: there are no user overrides to record.
* CLI: diff the filament slot before load_default_gcodes_to_config
The process and machine slots compute their different_settings_to_system column
before load_default_gcodes_to_config(); the filament slot did it after. That
call materialises absent gcode keys via option(..., true), and
DynamicConfig::diff only compares keys present in both configs -- so a gcode key
the resolved leaf did not carry would go from 'not compared' to 'compared as
empty against the parent' and land in the column as an override the user never
made.
Hoisted into a local above the call, guarded by load_filament_count > 0 so the
work is skipped exactly where it was before, and assigned at the original site.
The diff now also runs before config.erase("filament_settings_id"), which is
immaterial: cli_different_settings already filters filament_settings_id along
with the other *_settings_id keys.
This is a consistency fix rather than a demonstrated defect -- resolve_preset
merges the parent config, so in practice the gcode keys are already present on
both sides and the diff is unaffected. It removes the dependence on that
invariant, which the other two slots never had.
Reported by HanifKoh in review of #15595.
# 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)
A miss used to cost a preprocessor pass for the hash and then the real
compile. With the depend mode ccache hashes the include list the
compiler reports, so a miss costs only the compile. Ninja already asks
every compiler here for that list.
Clang records the modification time of every input in the precompiled
header, so a fresh checkout produces a different header and every file
that includes it misses the compiler cache. -fno-pch-timestamp makes the
header reproducible, and pch_defines lets ccache cache the header itself.
The precompiled header no longer has to be turned off when the cache is
on.
Every CI leg compiled the whole tree from scratch, 42 to 57 minutes of
each build job. Objects are now cached with ccache, one entry per leg
kept on the branch that built it: a push saves the cache and drops the
previous entry, a pull request restores main's and keeps nothing.
The precompiled header is turned off whenever the cache is on: Clang
stamps it with the build time, so every file including it missed. With
it off, a warm run hits 98.5 to 98.9 % of compiles and the compile steps
take 1 to 4 minutes; a cold run costs 25 to 60 % more than before, and a
change to a widely included header lands in between.
A build without SLIC3R_PCH had never been tried on macOS. Three files
used what pchheader.hpp happened to include: LocalesUtils.cpp needs
<sstream> and <iomanip>, and the two dialogs need <wx/tooltip.h>. The
GTK port's headers and libstdc++ pull these in transitively, the Cocoa
port's headers and libc++ do not.
# 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.
# 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?
-->
#15416 introduced a bug that caused system profiles to be copied over to
the system folder in the roaming folder on every startup.
# 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.
-->
<!--
> 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)
* Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill
For patterns with curved/turning anchor lines (Hilbert Curve, Octagram
Spiral), the bridge_over_infill algorithm produced incorrect results:
1. determine_bridging_angle: sampling curved anchor orientations
produced noise across all turning directions (0/90/180/270°)
instead of a single dominant one, yielding unstable bridge angles
with 180° spread. Fix: use the configured infill_direction + 90°
directly, bypassing the noisy sampling. The old blind +0.25*PI
(Hilbert) and +1/16*PI (Octagram) offsets are removed.
2. construct_anchored_polygon: curved Hilbert/Octagram anchors
intersected each vertical scan line many times at wildly different
Y positions, producing chaotic polygon sections — holes in random
places, bridges over air, rotated bridges. Fix: replace the curved
infill polylines with synthetic straight lines parallel to
infill_direction, spaced at the real infill line spacing
(flow_spacing / density). Lines are centered on the limiting_area
bbox center so that after rotation they span the full bridged_area.
Anchors are left at full bbox length (not clipped) to guarantee
every scan line finds an anchor.
Rectilinear and other straight-line patterns are unaffected.
Known limitation: some bridge edges may still terminate over air in
edge cases where the nearest synthetic anchor line is more than one
infill spacing away from the bridge boundary. This will be addressed
in a follow-up.
* fix: anchor internal bridges to actual sparse infill
Preserve real anchors across regions and align plane-path anchor origins with printed infill. Respect lower-layer rotation templates and model alignment, and sample curved bridge boundaries more finely.
Add regression coverage for anchor alignment, bridge angles and region isolation, with Orca comments explaining the geometry constraints. Verified 175 FFF tests before the comment-only follow-up; preserve CRLF in modified files.
* Fix internal bridge support contacts and separated infill origins
Restore anchor contact after bridge smoothing and share per-body pattern origins between anchors and printed infill. Recompute origins when preparation settings change.
Cover multiline counts 1, 2 and 3 and add regressions for printed bridge support, separated infill alignment and reslicing.
* Add explicit standard headers to PrintObject tests
* test: cover surface centering when infill settings change
Verify top and bottom Archimedean Chords and Octagram Spiral paths after switching centering modes or toggling separated infills. Compare reslicing against fresh slicing and document dependent infill invalidation.
* test: preserve directional surface infill when settings change
* perf: index layer islands for connected-body detection
* test: use public print pipeline for body centering checks
* build: clear 2 warnings - cast the NSTextField the class check already proved
mainframe_text_field is NSTextField* and was assigned a bare NSView*, which
Clang reports as -Wincompatible-pointer-types. Both assignments sit inside
if ([viewObject class] == [NSTextField self]), so the runtime type is already
guaranteed, and the line above the second one casts the same variable the same
way to call setTextColor. macOS only, since nothing else compiles this file.
* build: clear 6 warning categories from the clang-cl inventory
-Wmissing-braces (9). Aggregates whose first member is itself an aggregate.
GUID's fourth member is BYTE[8], so the trailing eight bytes take their own
braces. The others were reaching for zero-initialization with {0} and say {}
now. bbs_3mf's backup Task ends in an anonymous union, which needs braces of
its own; those braces initialize the union's first member rather than the one
named at the call site, so the RemoveBackup site says so in a comment.
-Wmacro-redefined (11). SendMultiMachinePage.hpp defines five names that
Preferences.hpp, PresetBundleDialog.hpp, ExportPresetBundleDialog.hpp and
TroubleshootDialog.hpp also define with different values, so the value in
force depended on include order. All nine of this file's DESIGN_ macros take
the SEND_ prefix it already uses for its own macros, values unchanged, so a
DESIGN_ name added elsewhere later cannot collide with it again. They read as
one page-local palette, a 900 to 400 gray ramp plus sizes, so the four with
no current readers stay: dropping them would leave gaps in a named scale. test_marchingsquares.cpp defines NOMINMAX,
which libslic3r already passes as a PUBLIC compile definition, so it takes
the #ifndef guard the other suites use.
-Wbraced-scalar-init (3). Two PushStyleVar calls resolve to the float
overload, so the braces were initializing a scalar. ConfigOptionFloatsNullable
already takes an initializer_list, so the inner braces did the same thing.
-Wmicrosoft-goto (2). Both gotos in copy_file_gui jump forward over the
initialization of size, dwRead and dwWrite, which only MSVC accepts. Those
declarations move up to join the others at the top of the function.
-Wunused-private-field (3). Every use of ColourPicker's m_clrData and
m_picker_widget is behind !defined(__linux__), so on Linux they are written
and never read; the members now carry the same guard. ParamsPanel's
m_size_move is read nowhere. Tab has its own, which is the one Tab.cpp uses.
-Wnonportable-include-path (2). BaseException.h asked for "stackwalker.h"
and the file on disk is StackWalker.h.
# 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?
-->
Addition to #14217 to support OTA updates when the zip content is an OPC
file.
# 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.
-->
<!--
> 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)
# Description
This PR introduces a publish workflow for sharing selected slicer
settings in .3mf projects without exposing or overriding the recipient's
complete printer, filament, or process profiles. A published file
specifies "requirements" for a model — printer, process, and per-slot
filament requirements — while importing it preserves the rest of the
recipient's workflow.
Publish dialog (File → Publish, Ctrl+Shift+E)
- Tabbed dialog (Printer / Filament / Process) with search, Select All /
Select Visible, and DPI rescaling.
- Per-material pages expose three kinds of content: individually
selected filament keys, a Full Publish toggle that embeds the slot's
entire filament preset, and required type and color rows for the slot.
- Publishing is allowed with no settings selected (the file then carries
only identity/requirement data).
- Reuses configuration value formatting extracted into a shared
ConfigValueFormatter.
Export
- Published 3MFs carry published, published_keys, and
published_material_keys metadata.
- A minimal export mode serializes only the selected values, material
identity, and plate geometry, omits embedded preset files, and masks
full-publish vector options to the author's slot so unrelated slot data
never leaks into the file.
Loading
- Only author-selected settings are applied; the recipient's presets are
otherwise preserved and structural/non-publishable keys are protected
(skipped keys are reported).
- Material settings match by filament ID, type, vendor, and slot.
Per-slot type requirements keep a matching receiver material, replace a
mismatched slot with the first same-type library filament (with a
notification), and fall back to a temporary embedded preset or skipped
keys when no match exists. Required colors are applied regardless of the
type match.
- A published 3MF loads as a new project: its path is not adopted as the
project filename (Save prompts instead of overwriting the shared file),
the title reverts to "Untitled", and the published metadata is stripped
so a later save produces a normal 3MF.
- Customized-preset and modified-G-code warnings are suppressed, since
the author's presets and G-code are not applied.
- Previously published 3MF files continue to load through the existing
matching path.
# Screenshots/Recordings/Graphs
<img width="1405" height="734" alt="publish_dialog"
src="https://github.com/user-attachments/assets/96b833c1-2b86-4c09-92b4-a4471a567e62"
/>
[publish_dialog_settings.webm](https://github.com/user-attachments/assets/acf22dd9-ae9b-4a58-b77d-8c2d9ffbc7a5)
## Tests
Added tests covering:
- Coverage for export slot masking, metadata round-trip,
type-match/replace/fallback semantics, slot growth, and skipped-key
reporting.
get_extruders() and estimate_wipe_tower_size() already ask whether any
instance of an object sits on the plate; the support-less extruder scan, the
mixed-filament risk check and the nozzle/filament compatibility check still
tested instance 0 only, so an object whose copy - not its original - was
placed on the plate was skipped by all three.
An instance added with "+" was never registered with the plate it landed on,
and moving an instance only re-registered instance 0 of its object, so a copy
dragged onto another plate stayed unknown to that plate's registry. The
plate's filament list, its wipe tower preview and the position clamp all read
that registry, so a multi-filament copy moved onto a single-filament plate
drew no tower there and its tower position was never clamped.
Register new copies at creation, notify exactly the instances a move changed
(every instance of the object when one of its parts moved), and drop the
registry entry when a copy is removed again.
build: clear eleven single-site clang-cl warning categories
Each of these is the last site left in its category, and every one is the
compiler saying it cannot tell what the code meant. Nothing here changes
defined behavior.
- OrcaSlicer_app_msvc.cpp printed a DWORD with %d
- StackWalker.cpp ran delete[] through an LPVOID
- ToolOrdering.cpp used a bare ; as a deliberate skip loop's body
- WipeTower.cpp had finish_block_tcr = finish_block_tcr, so the branch that
reached it did nothing. Folding the condition into the enclosing if leaves
the other branch untouched
- GCodeProcessor.cpp had an else binding to the inner if while the outer if
carried no braces
- AmsMappingPopupUpdate.cpp wrote >= 1 || <= 3 where its own comment says &&
- CalibrationWizardPresetPage.cpp left max_decimal_length unset through a
pair of conditions that cover every value but not visibly so
- DevManager.cpp bound map elements to pair<K, V> rather than
pair<const K, V>, copying every one
- SyncAmsInfoDialog.cpp had extraneous parentheses around a comparison
- Http.cpp had if (speed > 0.01) speed = speed;. speed now starts at 0 as
well, because curl_easy_getinfo leaves the target untouched when it fails
and the value reaches Progress either way
- SnapmakerPrinterAgent.cpp truncated npos into an unsigned int, so the
!= npos guard was always true. A colour with no # still yields 0, because
the wrap produced 0 as well
Nine categories go to zero. -Wtautological-overlap-compare and
-Wsometimes-uninitialized reach zero when #15583 merges their second site.
config_substitution_rule is a const enum with a constant initializer, so a
lambda can read it without capturing it. Capturing it explicitly is what
-Wunused-lambda-capture reports.
The category was taken to zero by #15417 and merged on 2026-09-02. These
three sites arrived on 2026-09-08 in bcb4f17d9a, "fix(cli): resolve inherited
presets through vendor manifests" (#15438). All three lambdas still read the
value, which needs no capture and is unchanged.
fix: report the real error when a Windows G-code export fails
copy_file built its failure message as "Error: " + errCode. Adding a DWORD
to a string literal is pointer arithmetic, not concatenation, so the pointer
lands errCode bytes into an 8-byte literal and runs past its end for any code
above 7. std::string then calls strlen on it and throws length_error, and the
catch(...) in BackgroundSlicingProcess::finalize_gcode replaces the diagnosis
with "Unknown error occurred during exporting G-code."
Every code a user is likely to hit is past the end: write-protected media is
19, no media 21, a full disk 112, and a destination held open by another
program 32. Codes 1 to 7 stay inside the literal and produce a truncated
message instead. So the "Maybe the SD card is write locked?" text has not
been reachable on Windows since this path was added in #2923.
Now that it is reachable, that guess only fits removable media, so it is
conditional on m_export_path_on_removable_media. The existing string is
untouched and keeps its 23 translations; the fixed-drive case adds one string.
* build: clear 2 warnings - a precedence bug and an arm64-only pragma
Both were found by promoting every warning to an error across the CI matrix.
Neither is reported by clang-cl on Windows x64, which is the configuration the
#15374 inventory measures.
LineSplit.hpp reserved with path.size() + closed ? 1 : 0. Addition binds
tighter than ?:, so that parses as (path.size() + closed) ? 1 : 0, and the
function returns early when path is empty, so the condition is always true and
the reserve is always 1. The vector then grows by reallocation instead of
reserving once. Output is unaffected, since reserve only sets capacity.
Reported by Clang on Linux, macOS and Flatpak; GCC does not diagnose it.
Int128.hpp declared #pragma intrinsic(_mul128) under _WIN64, which is defined
on Windows arm64 as well, where that x64 intrinsic does not exist. The call
site at line 190 is already guarded on _M_X64 and carries a comment saying
ARM64 has no _mul128, so the pragma now uses the same guard. x64 is unchanged
because _M_X64 is defined there.
* build: clear 1 warning - CLI error label prints 1 instead of a name
construct_assemble_list is a function, so streaming it converts the function
pointer to bool. When that catch block fires the CLI prints "1: <message>".
This line was already fixed in #5963 and came back in the wholesale revert of
that PR two weeks later, which was reverting an auto-orientation regression
somewhere in its 184 files. The string is restored exactly as it was merged
then.
Both are in our own code and neither shows up in a clang-cl or clang census,
so the Windows and CI matrices have never reported them.
FillRectilinear.cpp draws two trapezoid diagrams whose lines end in a
backslash, which continues a // comment onto the next line. GCC calls that a
multi-line comment. The diagrams are now block comments, where the rule does
not apply, and the drawings are unchanged.
CutObjectBase has a user-provided operator= and a virtual destructor, either
of which deprecates its implicitly generated copy constructor. bbs_3mf.cpp
copies the type through CutObjectInfo. The copy constructor is now declared
and defaulted, leaving the class with no implicit copy member. Move
operations were already suppressed by the user-provided operator=, so nothing
changes there.
Every edit makes the precedence the compiler already applies explicit. None
of them regroups an expression, so behavior is unchanged at all eight sites.
Strip parentheses and whitespace from the diff and the token stream matches.
GCodeProcessor.cpp:1472 tests == where the symmetric clause below tests !=,
which reads like a typo and is not one. A comment now explains why.
OrcaSlicer.cpp:4760 was the only judgment call. Its leading !is_seq_print is
bare while both operands are parenthesized, so the written form matches what
the compiler does. Kept rather than guessed at.
ImGui::Text and ImGui::TextColored take a printf format, so these six sites
passed data where a literal belonged. A % in that data reads a vararg that
was never supplied.
Three sites in GLCanvas3D's paint toolbar passed filament text, which comes
from the filament preset config and is user-editable. Two more passed
translated strings, where a % in any of the 23 catalogs does the same.
GLGizmoSimplify passed its progress label.
That label had been built with an escaped %% because it was being used as a
format string. Passing it as an argument instead needs a single %, so it
still renders as "42%".
ToUTF8() returns a buffer class, which converts to const char* for a named
parameter but not through varargs, so those two sites need .data().
GLGizmoSimplify.cpp:335 is unchanged, because _u8L("%d triangles") is passed
with a real argument and has to stay a format string.
Fix two stack buffer overflows in ADMesh stl_read (solid name + MW parse)
Bound the ASCII-STL solid-name fscanf scanset to the buffer size, and bound
the OrcaSlicer-specific "MW" metadata sscanf %s conversions to their buffers:
- fscanf(fp, " solid %[^\n]", solid_name) -> %255[^\n] (solid_name[256])
- sscanf(mw_position+3, "%s %s %s", ...) -> %15s %127s %15s
(version_str[16], model_id_str[128], country_code_str[16])
Both are reachable by opening a crafted .stl and overwrite saved stack state
(instruction-pointer control on the no-PAC arm64 macOS build). The solid-name
defect is inherited from the shared ADMesh loader (bambulab/BambuStudio#12153);
the MW parse is OrcaSlicer-specific.
Co-authored-by: Kevin Finisterre <kfinisterre@KevinsMacStudio.localdomain>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
# 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)
A plain CLI slice ran none of the placement sites, so a stored or
default tower position that no longer fits the tower the plate needs
went straight to the generation-time error. The slice loop now applies
the same clamp the GUI applies on reload to every plate it is about to
slice, skipping only plates that print no tower: by-object plates with
more than one instance, and plates whose footprint estimate is empty
(which covers single-filament plates without smooth timelapse, wrapping
detection or a raft). The plate's filaments come from the same
config-driven derivation the estimate uses everywhere else.
The two arrange sites read the brim width from the right option when
padding the default position; an auto brim uses its 8 mm cap there,
since the object heights are unknown before the estimate runs.
The preview brim, the placement margin and the pre-generation validation
warning each decided on their own whether the tower has a Type2 cone
base, reading the wall type and cone angle three different ways. The
preview's read cast the preset's enum to ConfigOptionEnum<T>, which a
preset-shaped config never holds, so the cone base was never previewed.
estimate_wipe_tower_first_layer_outline now answers that question once,
beside the footprint estimate, from the config and the resolved planner;
all three sites take the outline from it. The libslic3r case reads the
outline off a preset-shaped config, where the old cast came back empty.
The validator forces a two-filament print with the prime tower on and
slices it at the config default position (x 15, y 220), which lies off
any bed shallower than the tower. It calls validate() but slices
regardless, so the off-plate tower was exported silently; with the
generation-time footprint check it is rejected instead, and 522 of the
1013 printer presets failed the slice check.
The validator now positions the tower the way the GUI and CLI do before
slicing: beside the centred cube, clear of the edge exclusion strips some
beds carry, then pulled inside the printable outline by the tower's own
estimated footprint.
The clamps and validation work from estimates. Once the tower is
generated, _make_wipe_tower re-tests the exact first-layer footprint,
brim and cone base included, against the printable area and the
exclusion zone, so an off-plate tower fails with a clear error instead
of exporting unprintable G-code. The rectangle-wall mesh footprint
learns the Type2 cone base so that check and the post-generation
validation see the real outline.
Pre-generation, validation hard-checks the body plus an explicit brim
and warns on the estimated auto brim and cone base with the existing
"may collide" strings, so the user hears about a marginal position on
the first slice rather than only at generation time.
Two fff_print fixtures that print a tower at the default position move
it onto the 200 mm test bed, as the multifilament fixtures already do:
the shipped default y of 220 is off that bed, and the backstop now says
so instead of exporting the tower.
Smooth timelapse no longer charges a prime volume it does not purge. A
tower printed with no tool change is exactly the idle depth: the
stability minimum for Type2, the wrapping detection depth for Type1.
Charging a full prime_volume on top made the previewed and arranged
tower deeper than the one that is printed.
The Type2 half of "a tool change reserves a tower whatever the purge
volumes resolve to" arrives with the base commit; here it only has to
survive the planner split, since Type1 already reserves per filament.
The wipe tower filament only joins the tool ordering when there is a
tower to join, which is the has_wipe_tower() half of the guard
Print::extruders applies.
The shared estimate reserved every tower with one volume-per-purge rule
and the stability floor. Both planners do more: WipeTower (Type1) wipes
each filament's own prime volume in whole lines, one block per
adhesiveness category sized by its worst layer, rams the leaving
filament at every nozzle change, and squares a rib tower from the
planned depth; WipeTower2 (Type2) spaces its lines by
wipe_tower_extra_spacing, not the Type1-only infill gap, and its extra
flow cancels out of the depth. Both extend the ribs rather than the body
below the stability minimum, size every layer including a thinner first
one, and lay the brim in whole loops, WipeTower reporting half a spacing
of line width on top.
All of that now lives in estimate_wipe_tower_footprint, fed the planner
(resolve_wipe_tower_type mirrors Print::wipe_tower_type and the CLI's
Bambu Lab detection) and the filament ids rather than a count. Print
passes its own tool set; the PartPlate adapter derives the plate's ids
from the passed config and treats an explicit count as a floor, so the
CLI's count-only callers size per filament too. The placement clamp also
reserves a Type2 cone's base bulge, which the body box does not cover.
The planner-mirroring helpers sit beside the planners in WipeTower and
WipeTower2 so the two stay in sync; the libslic3r cases pin them to
footprints measured from generated G-code.
Validation grows the estimated body by the brim before the tower is
generated, so a tower whose brim leaves the bed is rejected up front
instead of at export. The scene reload re-clamps the stored position,
since set_default_wipe_tower_pos_for_plate does not rerun when painting
changes the filament count. The rectangle-wall footprint polygon gets its
two missing brim corners (it was a skewed quad), so the post-generation
check covers the whole brim.
A raft is not a reason to reserve a tower. Print::apply runs
normalize_fdm_2, which clears enable_prime_tower for a plate that purges
one filament unless smooth timelapse or wrapping detection is on, so a
single-filament plate with a raft prints no tower at all and the estimate
was reserving bed area for one. Drop the input; need_wipe_tower is now
exactly the two exceptions normalize_fdm_2 honours, named there so the
next reason added has to be checked against it.
The GUI preview and the validation containment check each re-derived
"is a tower printed here" from the filament count instead of reading the
estimate, so both missed the towers printed with no tool change to purge
for. They now take the answer from the footprint, which is the drift this
shared estimate exists to remove. A tower that is not printed estimates to
zero, so its hull is degenerate and every check on it passes trivially -
the containment check needs no gate of its own.
WipeTowerData::width was written only by the pre-generation estimate and
left at zero for the whole post-generation life of the Print, while its
neighbour depth held the real value. Set it from the generator in both
branches.
The plate's height scan transformed every model part's full mesh per
instance on each scene reload, discarding all but the z extent. The
cached convex hull has the same z extent.
A plate loaded from a sliced .gcode.3mf holds no objects and its filaments
live in slice_filaments_info; the config-taking get_extruders overload
returned an empty list for it, which sized the tower for a placeholder two
filaments. It now answers the way the wx overload does, without reaching
the plater.
Also drop estimate_wipe_tower_size, which has no callers.
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.
* 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>
* 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.
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.
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.
# Description
This PR redesigns `filament_id` across OrcaSlicer's profile library, so
that one filament
product now carries one consistent id everywhere it ships, instead of a
hand-written value that
unrelated materials routinely shared. Minting scripts and CI checks come
with it so future
profiles comply by construction: a new filament takes its id from the
tool, and the checks
reject a hand-written, duplicated or drifted one before it can merge.
Unique, non-duplicated ids are a precondition for AMS-style spool
syncing to be dependable —
the id is what a printer matches a physical spool against, and while two
products share one, the
match is a coin toss. This PR lays that groundwork. A follow-up PR will
publish an OrcaSlicer
materials reference on the wiki, giving every system profile one place
to point at.
`filament_id` names one filament product, and it is what a device
matches a physical spool
against: Bambu AMS, Creality CFS, the Qidi box, Klipper and Snapmaker
all resolve a tray to a
preset by id alone, first hit wins. Those ids were written by hand, and
on `main` 99 of them
stand for 674 different products — `GFL99` alone covers 132, from
Anycubic PLA to Bambu PLA
Matte. Every consequence is silent: a spool resolves to whichever preset
happens to load first,
tray names and support-material flags are read off the wrong material,
and the second preset
holding a duplicated id disappears from the tray-edit dialog entirely.
The id is now a hash of `(filament_vendor, filament_type, filament
name)`, so one spool product
carries one id in every bundle that ships it, two vendors shipping the
same product converge on it
without coordinating, and a collision cannot be authored by hand. Every
ambiguity across 48
vendors is fixed rather than excused — no grandfather list and no
per-vendor carve-out, Bambu's
bundle included — and the resulting landscape is frozen in
`scripts/filament_id_snapshot.json`,
so a change to any filament's identity lands as a reviewable diff to one
file. CI now runs the
duplicate-subtype validation tree-wide instead of over Bambu only.
Letting the id follow the product meant correcting the identities
themselves. Generics that
shipped under a vendor prefix now have one name and one id everywhere
(`Blocks Generic PETG` is
`Generic PETG @Blocks`), presets whose `filament_vendor` or
`filament_type` contradicted the
spool are fixed, and duplicate pairs are collapsed onto the
better-configured survivor. Renames
carry `renamed_from`, so existing projects and user presets keep
resolving.
Bambu's printers, its AMS and its cloud know only Bambu's own catalog
ids, so those ids leave
the profiles entirely. The Bambu bundle mints like every other vendor,
and the printer agent
swaps in the catalog value only where an id crosses to or from a Bambu
printer — outbound MQTT
and FTP, the AMS mapping sent with a job, the ids written into a 3mf the
printer will read —
mapping back on the way in, so status messages, SD-card prints and
projects saved by an older
Orca or by BambuStudio all still resolve. The correspondence is
generated from BambuStudio's
own shipped bundle by `scripts/update_bambu_filament_ids.py`; an id with
no row is forwarded
untouched, a missing or malformed map degrades to no translation rather
than taking the app
down, and an agent whose printers already speak Orca's ids translates
nothing.
Two device-side bugs this work surfaced are fixed here as well. The Orca
Filament Library was
missing from the AMS material and calibration dialogs, which treated a
filament with no
`compatible_printers` as compatible with nothing while the rest of the
app treats it as
compatible with everything; and Creality CFS sync on a K2-family stock
0.4 nozzle picked the
wrong preset, an imprecise matcher that duplicate ids had been masking.
`docs/HLSD/filament_id.md` is the authoring rule for all of this.
`scripts/orca_id_tool.py`
mints both `filament_id` and `setting_id`, replacing
`assign_vendor_setting_ids.py`, and the
local `check_profile` scripts gained per-vendor scoping so one vendor
can be checked without a
tree-wide run. `scripts/tests/` covers the tooling; `tests/slic3rutils/`
covers the boundary
translation and the CFS matcher.
Ids change for most products and nothing forwards the old value, so a
tray or a calibration
record still holding one falls back to matching by filament type until
the filament is selected
once. Beyond the profile fixes above no print settings change, except
that a few presets stop
claiming printers a dedicated variant already covers.
# 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.
-->
`scripts/check_profile.sh`, the local twin of the "Check profiles" CI
job, with
`scripts/check_profile.bat` as its Windows entry point, passes on this
branch: the extra JSON
check reports no errors and no warnings, system validation and the now
tree-wide
filament-subtype check load all 66 vendors cleanly, all 1013 printer
presets slice, and
custom-preset validation passes against every fixture archive from
v1.9.0 to v2.4.2.
The id tooling has 176 unit tests (`python -m unittest discover -s
scripts/tests`). The C++
suites pass too — 332 in `tests/libslic3r` and 126 in
`tests/slic3rutils`, the latter including
the boundary-translation and Creality CFS matching cases added here.
<!--
> 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)
filament_id is the plain mint of the product triple (filament_vendor,
filament_type, filament name), and nothing else feeds it. The tooling used to
accept any salt iteration of a preset's own triple, and its minting policy
stepped past ids that other products held in the tree or in the snapshot, so
which id a product got could depend on history. Every "salt split" in the tree
masked a redundant preset rather than a real need, and no shipped id is
salted, so salting goes entirely: no salt parameter, no id policy object, and
--generate no longer reads the snapshot.
--check now holds every declared and every inherited id to that one value and
lists each preset that misses it, variants under a wrong root included, instead
of folding them into the root's error. Two products whose triples mint one id
is reported as a collision naming both, and --generate refuses to write it;
the remedy is a rename so the triples differ. The Bambu catalog map generator
keys its rows by the same mint rather than by what the tree already ships.
No id changes: all 913 ids in the tree are already the mint of their triple,
so --generate is a no-op and the snapshot is untouched.
"Snapmaker PLA-CF @U1" and "Snapmaker PLA-CF @U1 0.4 nozzle" are the same
product on the same printer: both pin "Snapmaker U1 (0.4 nozzle)", and only a
filament_id salt split kept them apart. The first came with the original U1
profiles (#10225); the second with the tool-changer rework (#15039), which
added the 0.4/0.6/0.8 nozzle variants and left the older preset behind.
Drop the older one and re-mint the 0.4 variant onto OFhQf8ou, the id its 0.6
and 0.8 siblings already carry, so the product holds one id across the three
nozzles; renamed_from redirects the retired name. Users of the retired preset
get the #15039 tuning - 39 keys differ, including nozzle 230 -> 240 C, plate
55 -> 65 C and vitrification 150 -> 45 C - which is what the 0.6 and 0.8
variants already ship.
"Snapmaker PLA-CF @U1 base" stays. #15039 orphaned it by having the nozzle
variants inherit fdm_filament_pla directly, so it is unreferenced, but it is a
base in the vendor's own convention - 10 of the 14 @U1 families with nozzle
variants wire them to one - and it holds U1 loading and cooling values the
variants never set. Wiring them to it would change slicing output, which is
Snapmaker's call to make, not this PR's.
Claude-Session: https://claude.ai/code/session_01Q3zm9HuyskkSb4hynviV99
"Update re:3D profiles" (#13750) added the tuned "@0.4/@0.8 nozzle" and
"@0.8/@1.75 nozzle" variants but kept the five original un-suffixed presets,
trimming each to a stub that overrides only filament_vendor while still
claiming every printer of both nozzles. On any re3D printer the stub was
selectable alongside the ~50-key variant that actually tunes the material,
which is what forced five filament_id salt splits to keep the pair apart.
Drop the stubs. The variants already carry the products' ids, so nothing is
re-minted; renamed_from redirects each retired name to the smaller-nozzle
variant, and "re3D rPETG @0.8 nozzle" additionally takes over the
"re3D Greengate rPETG" mapping that lived on the deleted umbrella. Each
printer model's default_materials now lists the variants for its own
nozzles rather than the stub.
Cubicon's nine "@base" presets were the only instantiated ones of the 196
"@base" filament presets in the library; the other 187 are instantiation:false.
Being selectable, and claiming xCeler-I and xCeler-Plus, they put two presets of
one product on those two printers - which is what forced the nine filament_id
salt splits: one product deliberately kept on two ids so AMS matching stayed
unambiguous.
Flip them to instantiation:false and drop the compatible_printers, setting_id
and filament_settings_id that a base has no use for, matching what the other
150 bases carry. The three "@Cubicon xCeler-{I,Mini,Plus} 0.4 nozzle" variants
keep one printer each, so every printer still sees exactly one preset per
product and they converge on its single id; the nine salted ids retire.
A base is not added to the preset collection, so user presets that inherited
the selectable "@base" - the v2.3.2-v2.4.2 fixtures have one per material -
would lose their parent. renamed_from on the xCeler-I variant, the first model
the base ever claimed, redirects them; a preset built on "@base" while printing
on an xCeler-Plus narrows to the xCeler-I.
The six default_materials / default_filament_profile fields named
"Cubicon PLA @Cubicon xCeler-<model>", dropping the " 0.4 nozzle" the presets
actually carry, so none of them ever resolved; each model now names its own
variant.
Claude-Session: https://claude.ai/code/session_01Q3zm9HuyskkSb4hynviV99
On macOS wxWidgets reports Ctrl+left as a synthetic right button, which is
what made Ctrl+drag pan the canvas. #14999 added an unconditional correction
of the event's button state from wxGetMouseState(), which reports the
physical buttons and knows nothing about that synthesis, so the synthetic
right button was overwritten with a plain left button on every event.
Ctrl+drag then matched the left button mapping and rotated instead of
panning.
Apply the correction only when the event carries no button state at all.
On macOS wx populates button state only for the mouse-down and mouse-dragged
event types, which are also the only ones the Ctrl+left translation touches,
so the ImGui capture fix keeps every event it was added for.
Fixes#15214
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
A filament_id is now exactly what the preset's own filament_vendor,
filament_type and filament name mint, wherever it inherits from. Inheriting
settings no longer limits what a preset may claim, so the checks that policed
inheritance are gone, and so are the four grandfather lists that held thousands
of presets as permanent exceptions. The snapshot records sanctioned state rather
than excuses: one entry per id, carrying the product it names beside the presets
claiming it.
Profiles that disagreed are corrected instead of excused. The Elegoo TPU and
PAHT roots were named for a different product than all of their variants and
become TPU 95A and PAHT-CF; Elegoo PET-CF gains the filament_type its variants
already set; the Snapmaker breakaway support presets get an id of their own
rather than riding the PVA chain; and a BBL preset name carrying a doubled space
is fixed behind renamed_from. Their ids re-mint from the corrected identities.
The tooling and the design note also drop the word "family", which invited
reading a brand's Lite and Pro spools as one id.
No change to slicing output — only filament_id values, three preset names, the
inherits lines following those renames and the vendor indexes move.
Orca content-addresses every system filament, Bambu's included, but a printer,
its AMS and its vendor's cloud know only that vendor's own catalog ids. The
printer agent now translates between the two: outbound MQTT and FTP traffic, the
AMS mapping sent with a print job, and the ids written into a 3mf bound for the
printer all leave in the printer's own ids, while status messages, loaded
projects and SD-card prints arrive in Orca's. An id with no mapping passes
through unchanged, and an agent whose printers already speak Orca's ids
translates nothing at all.
Bambu's map is generated from BambuStudio's own shipped bundle; a missing or
unreadable file leaves every lookup an identity rather than taking the app down.
The profile check validates the map's shape, and profile CI now runs on the paths
that can change it. docs/HLSD/filament_id.md records the places the map
deliberately does not reach.
Two harness fixes, both paid for by hours of chasing product bugs that were not there.
ROUNDED RECTANGLE. It is the shape the user reported and the gate could not reach it:
the rectangle family binds R to CornerRect and leaves the other modes in the toolbar
flyout, so the three-step Width -> Height -> Radius chain was never exercised.
rung_rounded_rect() arms it over MCP the way the offer menu does. The verb id is the
OFFER id `sk_rect_rounded` — `design_rect_rounded` is the ACTION name, run_verb
throws on it, and the tool silently stays Select; a run that misses that draws
nothing and still reaches its assertions, so the rung arms AND verifies.
THE NETWORK PLUGIN MODAL. GUI_App::post_init() re-raises "Bambu Network Plug-in
Required" from an IDLE event, after any startup sweep has closed it, and
ShowModal() runs a nested event loop: the app is alive, its window is on screen,
and the MCP socket answers nothing. That is indistinguishable from a hang and was
investigated as one, with gdb, twice — the attached stack finally read
ShowModal <- show_network_plugin_download_dialog <- post_init. Seeding
`installed_networking` false stops the whole networking path, so the dialog never
exists to be swept.
Also: check() returns its verdict, so a rung can abandon itself when a precondition
fails instead of asserting into a dead end.
38 checks hold on behemoth against e5659e0f0f.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
check-gui-sketching.py located the value field by hunting for a small top-level
window and clicked into it before typing. Both halves are now wrong, and the second
was always a problem:
- field_win() cannot find a field that is not a window any more, so every check
built on it silently became one that cannot fail. Replaced by field_open(),
which asks the app: sketch_describe's `editing` is value_field_open().
- focus_field() clicked into the field first, and its own docstring said why —
"WITHOUT THIS THE TYPED VALUE IS SILENTLY DISCARDED". That workaround is exactly
what made this suite blind to the defect the user reported: a ladder that clicks
the field first can never notice that typing WITHOUT clicking is broken. Now a
documented no-op; the field is in the canvas and the canvas has the keyboard.
Measured on behemoth against e5659e0f0f: 64 checks pass, 2 fail. The two are
polygon regularity and area (sides {29.999986, 29.975164, 30.0}, area 2336.98 vs
2338.27) — geometry tolerances, nothing to do with typed values; before this change
the same run failed 5, all of them dimensions and constraints that never received
their value because focus_field() was clicking at a window that no longer exists.
No baseline exists for the remaining two, so they are reported, not claimed as
pre-existing. The suite also still stops at D3 with the app gone; tracked separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Rebases the in-canvas work onto cad-mainline and finishes it. The field is drawn
by ImGui inside the GL canvas instead of being a borderless top-level wxFrame.
WHY THE FLOATING FRAME COULD NOT BE FIXED. Whether a borderless top-level may hold
the keyboard is the window manager's decision, and it differs per desktop: openbox
grants it, mutter refuses it, macOS denies key status outright. Seven workarounds
fought that and one cost a macOS regression. Drawn inside the canvas there is no
second top-level for anyone to refuse, so the question is never asked. The field is
fed exactly like every other ImGui widget in the app — GLCanvas3D::on_char ->
ImGuiWrapper::update_key_data -> io.AddInputCharacter.
MEASURED, on behemoth: the click-edit ladder holds 28 checks — Line, Rectangle,
Circle, Slot, Polygon, Ellipse, Arc, and click-to-edit on a placed dimension label
— typing with NO click into the field first, committed == typed != prefill every
time, and 27 [UX] imgui_char lines showing the characters arriving.
WHAT WAS ACTUALLY WRONG. Not the field. The belief that "characters never reach the
ImGui InputText" came from the harness: the ladder was delivering keys with
`xdotool type --window` (XSendEvent), which GTK discards, so no build of any kind
could have received them. The new probe in ImGuiWrapper::update_key_data — the one
place ImGui is ever handed a character — is what separated that from a real defect,
and it stays, because a canvas-side probe provably cannot answer the question:
GLCanvas3D::on_char is bound later than any constructor-time probe, wx runs handlers
in reverse bind order, and on_char returns without Skip(), so such a probe is silent
whether or not the key arrived. A day was lost reading that silence as evidence.
Also drops DesignPanel's content-based forwarder and DesignCanvas::inline_type_char.
They were the right rule for a field that could not be focused; with the field
inside the canvas there is nothing to forward, and keeping them would have masked
whether the normal path works.
STILL UNVERIFIED: behaviour under mutter itself. Neither focus-stealing-prevention
WM available here survives long enough to judge — metacity SEGVs ~20s in and xfwm4
dies with BadWindow on SetInputFocus, both before the sketch opens and both
unrelated to this field. The design's claim is structural rather than measured: no
second top-level means no focus to refuse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
focus= in [KEYTRACE] was never a class name. GetClassName() returns const wxChar* — wchar_t* in
this build — and the line cast that to const char* and printed it with %s, so it emitted the first
byte and stopped at the padding NUL. "wxGLCanvas" came out as "w". So did "wxWindow". Every focus
reading taken from this instrument for a whole day of diagnosis was a single character, and the
one question it existed to answer — WHICH widget has the keyboard — was the one it could not
answer. Printed through wxString now, and it says focus=wxGLCanvas: the canvas does hold wx focus
while the field is open, which removes the focus hypothesis for good.
Also here, and HONESTLY LABELLED AS INCONCLUSIVE: a wxEVT_CHAR probe on the canvas. It logged
nothing (cc=0), and the tempting reading is "the characters never reach the canvas". That reading
is not available, because the probe is bound in the DesignCanvas constructor BEFORE
GLCanvas3D::bind_event_handlers(), and wx runs the most recently bound handler first —
GLCanvas3D::on_char returns without Skip() exactly when ImGui consumes a character, which is
precisely the case under test. A silent probe is therefore consistent with ImGui consuming the
keys correctly AND with them never arriving. It measures nothing. Rebind it after
bind_event_handlers(), or instrument update_key_data itself, before believing anything about it.
Writing this down rather than acting on it: I came within one commit of "fixing" a mechanism I had
inferred from an instrument that could not see it, which is the same mistake as the focus= field
above and the same mistake that cost a whole session in September.
Deployed binary restored to 00d6c191dc (md5 0eeb9a58cef5).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Third measured step, and the last one I will take without a second pair of eyes.
set_as_dirty() BEFORE Refresh(): GLCanvas3D's paint handler returns without rendering when the
canvas is not marked dirty, so the previous commit's bare Refresh() posted paint events that drew
nothing and the frames stopped anyway. With both halves the pump sustains, and the trace shows the
field holding the keyboard frame after frame:
[UX] frame want_text=1 want_kb=1 active=1 buf=158.74
[UX] frame want_text=1 want_kb=1 active=1 buf=158.74 (repeating)
STILL OPEN, and now narrowed to one question: buf never changes. ImGui owns the keyboard and our
InputText is the active item, so what is missing is upstream of ImGui — the characters are not
reaching io.AddInputCharacter at all. The next thing to MEASURE (not to change) is whether
wxEVT_CHAR arrives at the GL canvas in the Design tab: DesignPanel's wxEVT_CHAR_HOOK Skips digits
while inline_busy(), but Skip only helps if the focused widget is the canvas, and nothing has yet
proved that it is at the moment the keys are sent.
Three attempts have now gone into this one point. Per the standing rule that is where solo
iteration stops.
Deployed binary restored to 00d6c191dc (md5 0eeb9a58cef5) — nothing from this branch is installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Measured, not reasoned. A per-frame trace of the ImGui state is what finally named the mechanism,
and it is a deadlock, not a focus problem:
[UX] frame want_text=0 want_kb=0 active=0 buf=158.74 <- frame 1: the widget is not active yet
[UX] frame want_text=0 want_kb=0 active=1 buf=158.74 <- frame 2: now it is
(nothing further) <- and the canvas stops
This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a frame,
from the active item; GLCanvas3D::on_char only calls render() when update_key_data() says ImGui
wants it. So: no frames -> WantTextInput never turns on -> no render on a keystroke -> still no
frames. The characters sit in ImGui's input queue and the field is exactly as deaf as the window
it replaced, for a completely different reason.
request_frame breaks the circle, and the same trace says so:
[UX] frame want_text=1 want_kb=1 active=1
That is the first time in this file's history that the value field has owned the keyboard without
asking a window manager for it.
STILL OPEN: the typed characters do not reach the buffer (buf stays at the prefill) and the frames
stop after nine. The pump is the suspect — on software GL request_repaint() calls m_canvas->render()
SYNCHRONOUSLY, so this asks for a render from inside a render; it needs to schedule one instead.
That is the next thing to measure, not to guess.
The deployed binary on behemoth is restored to 00d6c191dc (md5 0eeb9a58cef5), byte-identical to
the last good build. Nothing from this branch is installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
The decision (Tommaso's, put to him with the trade-offs): the field stops being a separate
top-level window, because whether such a window may receive typing is the window manager's
call and not ours. openbox grants it, mutter on his desktop refuses, and seven previous
workarounds fought that — one of them causing a macOS regression, and the test harness ending
up clicking the field before typing, which is a workaround no user can be asked to perform and
is exactly the "label value not editable" report.
DONE and proved on the rig: SketchInlineEditor is no longer a wxFrame + wxTextCtrl. It is state
plus an ImGui overlay drawn by DesignSketchTool::render(), at the same screen anchor, in the same
vocabulary as the dimension labels next to it (draw_dim_label is already an ImGui window). The
field opens where it should — [UX] open title=Length prefill=158.74 from the running app.
NOT DONE: typing does not reach it. ImGui is fed from GLCanvas3D's own key handler, so the keys
have to arrive at the canvas; giving the canvas wx focus when the field opens was not enough.
The remaining question is where a keystroke goes between DesignPanel's wxEVT_CHAR_HOOK and
GLCanvas3D::on_char in the Design tab, and whether the canvas repaints often enough for ImGui to
advance its input state. That is attempt three on this specific point, so it goes to a second
opinion rather than a third guess.
Also here: scripts/CAD/check-gui-click-edit.py, the ladder Tommaso asked for. It types into the
field WITHOUT clicking it first — the click is what check-gui-sketching.py's focus_field() does
and why that suite can never see this defect — and fails when the prefill is what gets committed.
It currently fails, correctly, on the above.
Not on cad-mainline: the deployed binary must stay the last good build until typing works.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
present_toplevel() already asked for focus with a server timestamp, but only when
the widget happened to be realized. An unrealized widget has no GdkWindow, so
there was nothing to read a timestamp from and control fell through to
wxFrame::Raise() — which asks for activation with GDK_CURRENT_TIME, i.e. 0.
Zero is exactly what focus-stealing prevention discards. metacity says it out loud
when a sketch value field opens:
Buggy client sent a _NET_ACTIVE_WINDOW message with a timestamp of 0
and mutter, same lineage, refuses it silently on the user's desktop. That refusal
is the reported defect: the field is visible, never receives the keyboard, and
Enter commits the as-drawn prefill.
So realize the widget and retry, and do NOT fall back to Raise() on X11 — a
timestamp-0 activation is refused anyway, and on some window managers it only
marks the window as demanding attention.
Not yet confirmed end to end: both window managers with focus-stealing prevention
available here abort on this frame — metacity at frames.c:1239, xfwm4 with
BadWindow on SetInputFocus as the frame is destroyed under it — so the ladder
cannot yet return a trustworthy verdict under one. Two WMs crashing on the same
borderless, repeatedly re-mapped STAY_ON_TOP frame is its own signal about this
design. Tracked in projects-1p5 and projects-40m.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
The reported defect: sketch dimension labels are "not editable" — you draw a
rectangle, its Width field opens, you type, and the as-drawn number is committed
instead. It affects every sketch tool, not just the rounded rectangle.
WHAT THIS ADDS
1. The arbiter (DesignPanel CHAR_HOOK -> DesignCanvas::inline_type_char ->
SketchInlineEditor::type_char). Routes a key by what it IS, not by who the
window manager focused: digits, sign, decimal separator and Backspace/Delete
go to the open value field, Enter/Tab commit, letters stay tool shortcuts.
This is FreeCAD Sketcher's rule (DrawSketchKeyboardManager::
detectKeyboardEventHandlingMode), and the reason its sketcher behaves the same
on every desktop: it never asks who has focus.
2. The [UX] trace (SNAPORCA_UXTRACE) in SketchInlineEditor: open/commit/refused/
cancel, with the prefill and what the control actually held at Enter. It did
not exist — the ladder below was written against a surface no build emitted,
so it could only ever report "nothing opened". typed == prefill on a commit is
the defect's signature and nothing else makes it visible.
3. A draw-then-edit trace in DesignSketchTool: four early returns can swallow the
value-field chain and from outside they are indistinguishable.
4. scripts/CAD/check-gui-click-edit.py — types WITHOUT clicking the field, as a
person does, across Line/Rectangle/Circle/Slot/Polygon/Ellipse/Arc plus label
click-to-edit, and asserts committed == typed != prefill.
5. scripts/CAD/focus-loop.sh — sync/build/assert on behemoth. NOT the orcacad-gui
rig: its image pins deps 216 non-CAD files behind cad-mainline, so today's CAD
sources cannot build there without a deps rebuild.
WHAT IS PROVEN, AND WHAT IS NOT
Green under openbox: 28 checks, every tool, committed == typed != prefill.
But openbox CANNOT adjudicate this bug and the ladder says so in place. There the
field always wins the keyboard, so the same ladder also passes against a binary
with the arbiter compiled out — measured twice. Two ways of removing the keyboard
were tried and both are recorded as dead ends: XSetInputFocus loses to the field's
own re-focus CallAfter, and XSendEvent (xdotool --window) is dropped by GTK, which
made every run red regardless of the code.
Under metacity — same focus-stealing-prevention lineage as the user's mutter — the
mechanism appears in the WM's own log:
Buggy client sent a _NET_ACTIVE_WINDOW message with a timestamp of 0
That is the activation being refused, which is exactly the reported symptom.
present_toplevel() already asks for a server timestamp, so a path is still falling
through to frame->Raise(), which sends time 0. That is the next thing to fix, and
it is tracked; the arbiter alone does not close it. metacity also aborts on this
window (frames.c:1239), so the gate needs a WM that survives before it can return
a verdict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
"Still cannot edit labels in rounded rectangles." Reproduced on the rig in a few minutes, and it
is NOT the window-manager defect the rest of this week has been about — it happens on openbox,
where typing into a value field works perfectly. The value was never the problem. The LABEL was
not there.
render_live_quotes picks the entity to speak for like this:
else if (m_selection.size() == 1) ei = m_selection[0];
if (ei < 0 || ...) return;
A rounded rectangle is EIGHT entities — four lines and four arcs — so selecting the shape makes
m_selection.size() == 8 and the pass returns before drawing anything. Its Width, Height and fillet
Radius are live labels and nothing else, so with them gone there is no affordance at all: no
number to click, no field to open, no value to refuse. The rule hid the characteristic quotes for
precisely the shapes that have nothing but characteristic quotes.
A plain rectangle looked fine only by accident. Typing into its auto-edit chain creates a DRIVEN
dimension, which render_dimensions draws from the annotation list, so its labels survive. The
rounded rect's W/H/R go through set_rounded_rect, which rebuilds the geometry and leaves no
annotation behind. Same for slot, arc-slot and polygon: every grouped feature was in this hole.
A selection that is entirely ONE feature now speaks through any member. The switch below already
keys off feature_of(ei) rather than the entity, so nothing else had to change.
Measured on behemoth :10, before and after, same binary path:
before 8 selected -> no labels at all
after 8 selected -> R26.6 / 117.4 / 150.7 drawn; clicking R26.6 opens Radius prefilled 26.60;
typing 8 gives R8.0 mm and visibly sharper corners.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
# Description
Under `core.autocrlf=input`, a Windows dependency build fails while
building CPython:
```
The system cannot find the batch label specified - begin_search
Cannot locate python.exe on PATH or as PYTHON variable
error MSB8066: ... exited with code 3
```
`git apply` inherits the caller's git configuration, so it rewrites the
patched `PCbuild/find_python.bat` to LF, and `cmd.exe` cannot resolve
`goto` labels in an LF batch file. `git init` already runs in the
extracted source, so setting `core.autocrlf` on that repository fixes it
and leaves the shared `PATCH_CMD` alone.
Only python3 is affected, its patch being the only one under `deps/`
that touches a `.bat`. `core.autocrlf=true` (the Git for Windows
default) and `false`/unset were never affected.
If a dependency build later fails with `patch does not apply`, delete
`deps/build` and rebuild. `git apply` is not idempotent, and that is
independent of this change.
## Tests
Visual Studio 2026 (18.6.3), `core.autocrlf=input`, built from an empty
`deps/build-dbg`:
| `deps debug` on | `find_python.bat` | CPython |
|---|---|---|
| `upstream/main` | 0 CRLF / 95 LF | fails at `begin_search` |
| this branch | 94 CRLF / 1 LF | builds |
It then stops at the debug staging step, a separate bug fixed by #15353;
with both applied it installs `libpython`. I hit this on a debug build,
but the patch step has no Debug/Release conditional.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Two reports, three defects, all three measured on the running app rather than reasoned about.
"Keyboard focus in drawing tool is broken so that now they are slow and cumbersome." After a
dimensioned entity the bottom-right readout chip — 119x31, borderless, a wxFrame — held the X
input focus. Pressing r produced NO [KEYTRACE] line at all: the key never reached the panel's
CHAR_HOOK. One bare canvas click moved focus back to the main window and the identical key armed
the tool. So every shortcut was dead after every dimension, and the way to get the keyboard back
was to click somewhere harmless. That is the whole of "slow and cumbersome".
Its sibling, the status chip, is a wxPopupWindow for exactly this reason and carries a comment
warning against turning it back into a frame. The readout was left a frame on the premise that
"it appears mid-gesture and the next input is the mouse" — which the measurement falsifies: the
chip keeps the last value on screen after the gesture ends, and a frame that has the focus does
not give it back. It is now a popup too, with the placement and the iconise/deactivate lifecycle
its sibling already needed, because an override-redirect window would otherwise sit on the bare
desktop when the app is minimised.
"Planes hide the bed." Literally true, twice over. The reference planes were half-extent 0.6 *
the bed's larger side — a square 1.2x the plate — and all three are drawn with depth testing
off, so they painted over the plate grid from edge to edge. 0.3 puts them inside the bed, which
is also the Onshape look the size was reaching for: a modest square at the origin, not a
tablecloth.
And the other half was mine. 3f52166e32 muted the bed for the duration of a sketch, on the
argument that a plate grid and a sketch grid are the same visual language. The argument is right
and the call was wrong, because there IS no sketch grid to take over. Pick XY, arm Line, and the
viewport was an empty grey field: no bed, no grid, no origin, nothing to judge a length or a
direction against. The plate grid was carrying the ground reference for the whole tab. The banner
already says where you are; taking the floor away as well only made the sketch harder to draw.
The Bed checkbox is the one thing that governs the bed, in every mode.
Verified on behemoth :10 with the rebuilt binary: focus after a dimension chain is the main
window, r arms Rectangle with no click in between, and the plate grid is under the sketch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Measured on the running app, not deduced. After a queued dimension chain the value field's frame
is left MAPPED on purpose (mutter refuses keyboard focus to a re-mapped window), so there is a
window in which m_open is already false and the frame is still on screen holding the X input
focus. GTK meanwhile reports that window inactive and routes nothing into the text control. Every
key then lands somewhere that cannot use it and will not give it back:
[KEYTRACE] key=27 ui_mode=1 inline_busy=0 <- the last key the panel ever sees
=== MARK press Delete === <- no trace line at all
xdotool getwindowfocus -> 0xe00404 86x60 <- the value field, still mapped
Delete, Esc and typing all read as dead, which is exactly the report. And nothing could recover
it: close(), cancel() and do_cancel() all return early on !m_open, so the one window still
receiving keystrokes was also the one window no code could dismiss.
is_mapped() asks the question the flag cannot answer, and dismiss() tears the frame down with no
m_open guard, since m_open is precisely what lies in this state. Esc inside the field falls back
to it — while the frame holds focus that handler is the only code the keyboard can still reach,
so if it refuses, nothing else gets a turn. Every close now hands focus back to the canvas
explicitly, because hiding a window does not move the X input focus off it. And inline_busy()
reports the union of "a value is pending" and "a frame is mapped", so Esc routes to the field
whenever one is on screen at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Three cues, because one is missed. A teal banner across the top of the viewport names the session
("Editing: Sketch N") and where its exits are; the printer bed is muted for the duration, since a
plate grid and a sketch grid are the same visual language and reading one as the other is how a
sketch gets drawn against the wrong reference; and N looks straight down the plane normal at the
current zoom, with the plane's own y axis as up, because no hand-orbit lands exactly square and a
sketch read at an angle is one whose right angles do not look like right angles.
The banner is an INDICATOR. Finish and Cancel stay on the single ribbon action bar — the tab had
three competing confirm surfaces once and that is not being reopened for a strip of colour. It
sits above the canvas rather than floating inside it: a child window over a wxGLCanvas is a native
window on GTK with no reliable stacking over GL, and being unmissable beats being clever.
The bed checkbox stays the stored preference and is restored on leaving the sketch; ticking it
mid-sketch still shows the bed, because that is a deliberate act and this is only a default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Two presses used to discard a live sketch. The key was answered in four places that could not
see each other — the inline value field, a sketch branch, a feature-card branch, and the canvas
— so a press aimed at one fell through to the next, and request_exit() carried a fourth layer
that deliberately let the SECOND consecutive press through to cancel_sketch(). The warning it
showed first did not help: the two presses are never one decision, the first is aimed at a field
or a tool and the second at whatever was underneath it.
The stack is now explicit. CadLevel (DesignInteraction.hpp) is four levels deep, the enum value
IS the LIFO depth, and cad_escape_level() is a constexpr function over a POD of four booleans —
so the ordering that is the entire contract is checked by static_assert at compile time, with no
window, GL context or event loop. DesignPanel::escape() acts on the one level escape_level()
names and on no other, and every Esc in the tab routes through it.
The destructive layer is gone from request_exit() itself rather than guarded at its callers, so
the guarantee cannot be re-opened by adding a route: a session holding geometry is left only
through Finish (keep) or Cancel (discard). Cancel now asks before discarding — it used to refuse
and tell the user to press the button they had just pressed, which meant a drawn sketch could be
kept but never thrown away.
Right-click also stops rewarding navigation with a menu: the offer needs BOTH budgets, released
within 200 ms and moved no more than 3 px, and the raycast uses the press position, so the menu
describes what was pointed at rather than where the camera stopped. Two budgets because drift
alone still popped a menu at the end of a slow, careful orbit.
docs/ux/interaction-model.md carries the state machine, the routing and the transition table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
* 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
# 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)
The tree-wide length check (freed from its BBL/OFL carve-out last commit)
was flagging 21 pre-existing SeeMeCNC files that no vendor index references
and that therefore never load, turning CI red for files with no bearing on
what ships. The rule now only fires on presets a vendor's filament_list
actually references; every .json under the vendor's filament directory is
still parsed through the duplicate-key hook, so that coverage is unchanged.
Also removed a duplicated BAMBU_MAP_PATH definition: update_bambu_filament_ids.py
now imports the constant from assign_filament_ids.py, which it already imports
several other constants from, instead of recomputing the same path independently.
Every vendor's filament_id declarations, Bambu's own bundle included, are now
minted and checked the same way: the GF* catalog space is reserved but
ownerless, format is validated unconditionally with no snapshot or BBL
exemption, and both --remint and the default assign pass treat a non-OF
declaration as one needing a fresh mint (so a future BambuStudio sync
self-heals instead of needing a manual pass). A new check validates
resources/printers/bambu_filament_ids.json against the tree it describes:
that it parses, carries its header, keys only OF ids, maps each Bambu id once,
and agrees with the tree on every product it shares. The redundant BBL/OFL
carve-out in the profile checker's length check is dropped too, so it runs
the same way for every vendor.
The BBL bundle itself hasn't been touched yet and still declares its old GF
ids, so TestRealTree.test_shipped_snapshot_matches_tree is expected to fail
here (209 declarations flagged) until the next commit re-mints the bundle
onto OF ids.
scripts/update_bambu_filament_ids.py derives resources/printers/bambu_filament_ids.json
from BambuStudio's own shipped BBL bundle (cloned from upstream, or read from a local
checkout via --bambustudio-dir), pairing each Bambu catalog id with the Orca filament_id
we already ship for that product where we ship one, else a freshly generated one. Nothing
reads the map yet; later work uses it to translate ids at the Bambu printer boundary.
* 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
Refresh detach-from-parent checkbox state
Allow the detach checkbox toggle event to propagate to the custom CheckBox control so it refreshes its bitmap after the value changes.
* fix: build_win.bat builds with whatever clang-cl is first on PATH
VsDevCmd appends the Visual Studio LLVM directory to the end of PATH, so
a standalone LLVM already on it shadows the Visual Studio one. -l -x
passed a bare clang-cl.exe for CMake to resolve, so the build ran on
whichever copy came first. For one reporter that was an LLVM 11, which
failed the compiler check before anything was compiled:
-- Check for working C compiler: C:/Program Files/LLVM/bin/clang-cl.exe - broken
lld-link: error: undefined symbol: __guard_eh_cont_table
The compiler is now resolved through vswhere and passed as a full path,
so PATH order no longer matters. CMake derives the linker from the
compiler directory, so lld-link follows. Only a configure passes it to
CMake, so -p and --no-configure resolve nothing and stay buildable on a
machine with no clang installed.
When Visual Studio has no clang toolset the script falls back to the
first clang-cl on PATH and names it. With none installed at all it now
errors with what to add, instead of failing later inside CMake. Every
clang-cl run that configures prints the compiler it resolved.
The suite gains a clang-cl fixture earlier on PATH than the Visual
Studio one, and an empty ProgramFiles(x86) to put vswhere out of reach,
which covers both fallbacks without touching the machine.
* fix: build_win.bat pointed at a solution file that is not there
The Visual Studio 2026 generator writes OrcaSlicer.slnx and the releases
before it OrcaSlicer.sln. The summary hard-coded the second, so the path
it printed after an MSVC build against 2026 was wrong.
The sweep now validates each printer with the filament that printer ships, so a run
over every vendor reports what a single-vendor run does. Validator only - no change
to slicing output or shipped profiles.
Revolve failed on a sketch whose profile was closed. Decoding the reported 3mf: four
entities forming a proper closed loop (joints open by 4.44e-06 mm, well inside
tolerance) plus one stray 1.82 mm Line at (-24.2, 80.3), inside the shaded region,
touching nothing.
The viewport's region_loops discards open chains ON PURPOSE — it exists to find
EXTRUDABLE regions — so the user saw one clean closed region. entities_to_wires kept
the stray as its own one-edge loop, so it returned two wires, and Revolve goes through
entities_to_wire which demands exactly one. Extrude would have failed one step later in
wires_to_face, because a one-edge open wire bounds no face. Same class as the tolerance
split fixed in 8b568b7b: the viewport and the kernel disagreeing about the sketch — this
time about what BELONGS to the profile.
entities_to_wires/entities_to_wire/build_sketch_wire take closed_only. It is not a
blanket rule: a SurfaceExtrude builds a sheet FROM an open profile and a Sweep PATH is
normally open, so all ten call sites are classified individually — true for the face
fallback, Extrude-taper, Revolve, the Sweep PROFILE and Loft profiles; false for
SurfaceExtrude/Revolve/Loft/Fill and the Sweep path.
A component counts as open when some welded node has DEGREE 1. The first attempt used
"the traversal did not return to its starting node", which regressed the bridged C
profile: a closed loop that also carries a second edge across the same two nodes has no
free endpoint, but its Eulerian walk ends elsewhere. Degree-1 is the property that
actually distinguishes a stray segment from a closed profile; the suite caught the
difference.
Behaviour change decided by Tommaso: a stray is IGNORED, not refused. The test that
required refusal dates from when ignoring meant falling through to a default rectangle —
geometry nobody drew. That fallback is gone, so ignoring now builds the circle the user
actually drew. Its assertion is updated with the reason.
The bridge round-trip test extruded an ENTIRELY open chain and "worked" only because
OCCT will make a face from an open wire. It gets a genuinely closed profile: the test is
about serialization, and deserialize_recipe recomputes, so the document has to be one
that legitimately builds.
Failures now say WHERE. sketch_open_ends reports free endpoints under the same weld
tolerance the wire build uses, and open_loop_message is shared by both throws, because
Extrude fails through build_sketch_face and Revolve through build_sketch_wire — enriching
only one would have left the commoner path the less informative one.
Kernel 66115 assertions / 608 cases green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Tommaso asked the question that names the real defect: if the sketch was open, why was
the same sketch shaded closed and offered for extrude? Because the two halves used
different tolerances. region_loops shades a region closed at 1e-3 mm; connected_loop
chained at 1e-3; the kernel welded at 1e-4 and OCCT matched vertices at 1e-7. The
2.28e-5 mm gap in the reported sketch did not cause that disagreement, it only made it
visible — and fixing the gap alone would have left the contradiction in place, ready to
reappear anywhere in (1e-4, 1e-3].
kSketchJoinTol now lives in SketchEngine.hpp and is the only place the number exists.
region_loops, loop_report, connected_loop and entities_to_wires all read it through
sketch_join_tol(). The viewport cannot promise a region the kernel refuses to build.
The welding is optional, because a kernel that silently closes loops should let you say
no: "Auto-close sketch loops" in Preferences, default ON, no restart. OFF means only
exactly coincident endpoints join — and since both halves read the same value, the
viewport simply stops shading the region closed, so an open loop is visible rather than
welded behind your back. No separate UI needed for that; it falls out of sharing one
number.
Details that matter. The kernel defaults to auto-close ON independently of the GUI, so
headless and MCP callers behave like the viewport instead of inheriting an unset
preference. With the tolerance at 0 the comparisons become <=, because OFF must mean
exact, not broken. OCCT never receives a zero vertex tolerance — it is clamped to
Precision::Confusion.
The preference is pushed from EVERY entry that starts a sketch session, not just
begin(): a Constrain session enters through begin_constrain / begin_constrain_entities
and uses region_loops and connected_loop, so a single push site would have left those
sessions running on whatever the previous one set. begin_imported_transform is excluded
deliberately — it works on imported regions, not chained entities.
Tests: a loop with one joint open by 9e-4 mm, given out of traversal order, builds a
closed four-edge wire; with auto-close off the same loop yields no wire; and an exactly
closed loop still builds with auto-close off, proving OFF means exact. Kernel 66104
assertions / 606 cases green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
An extrude built a solid the user never drew: three sides of the handle plus the arc
that bulges outside the outline, with the bowl's second arc and the left edge missing.
Two faults met. entities_to_wires added edges in ENTITY-CREATION order, so a partial
wire rejects the next edge even when the sketch closes perfectly; and one joint of the
reported sketch is open by 2.28e-5 mm, wider than OCCT's 1e-7 vertex tolerance and
wider than this function's own EPS of 1e-6, so that edge was refused on geometry too.
Neither showed up, because BRepLib_MakeWire::Add DROPS a disconnected edge
(BRepLib_DisconnectedWire + NotDone) while every successful Add ends with
BRepLib_WireDone + Done() — overwriting the failure. `if (!wm.IsDone()) return {}`
was therefore asking only whether the LAST edge connected. Six edges in, four out,
IsDone() true.
Endpoints now weld into shared nodes at one tolerance (kSketchWeldTol) used by BOTH
the union-find grouping and the wire build — they disagreed before, which is how a
joint gets united into a loop and then refused by the builder. Each node becomes ONE
TopoDS_Vertex, so the builder matches on identity instead of proximity, with the
vertex tolerance widened because BRepLib_MakeEdge::Init projects a vertex onto the
curve within that tolerance and a welded node sits up to the weld gap off its
neighbour's curve. Members are then walked in traversal order. Finally the result is
counted: IsDone() alone is not evidence, edge_count == members.size() is.
Arc geometry is untouched — the midpoint from (start_angle+end_angle)/2 and the
solver's angle reflow both measured correct and were never part of this.
The regression case carries the reported sketch verbatim, open joint included. It
fails 4 == 6 without the fix, which was measured, not assumed. Kernel 66092
assertions / 604 cases green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
* build: enable /Zc:lambda for MSVC
MSVC keeps its legacy lambda processor under /std:c++17, which rejects
reading a constexpr constant inside a lambda that does not capture it
(C3493). No other compiler requires that capture, and clang reports it as
an unused one, so the two cannot both be satisfied without the flag.
/Zc:lambda selects the conforming lambda parser that clang and GCC
already use. It is implied by /std:c++20 and /permissive-, so it is only
needed while we are on C++17. clang-cl is conforming already and does not
take the flag.
It requires VS2019 16.8, so build_release_vs.bat now says 16.8+.
* build: clear 237 unused lambda capture warnings
236 captures across 81 files, 142 of them `this`. Removing an unused
capture changes no behavior; clang does not report a capture whose type
has a non-trivial destructor, so nothing held only to extend an object's
lifetime is in this set.
Nine of them are the second half of the warning, "is not required to be
captured for this use", where the capture is a const or constexpr value
the body does read. Those depend on the /Zc:lambda change in the previous
commit. One of them, in FillRectilinear.cpp, had been worked around with
an #ifndef __APPLE__ guard around the capture list, which is now gone.
GUI_ObjectTableSettings.cpp captured its reset button only to read it
inside #ifdef __WXOSX_MAC__. That branch now takes the button from the
event it is already handling.
* build: fail configure on MSVC older than 19.28 instead of dropping /Zc:lambda
cl.exe answers an unrecognized /Zc: sub-option with warning D9002 and keeps
going, so on VS2019 before 16.8 the flag is silently ignored and the build
instead dies with C3493 in FillRectilinear.cpp, nowhere near the cause.
* fix: delete three locals that are now unused
Their only remaining use was the lambda capture this branch removed. The
Clang builds set -Wno-unused-variable, so the build never flagged them.
---------
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
* fix: prevent heap corruption in model repair with auto-backup
The CGAL model repair (fix_model_with_cgal_gui) runs on a worker thread
that mutates the live ModelObject (split / delete_volume / set_mesh).
Those mutators transitively call save_object_mesh(), which hands the
object to the auto-backup manager. The manager clones and serializes the
object on its own thread via an internal Model documented as "visit only
in main thread". Running that path from the repair worker races the
backup thread on the shared model, causing use-after-free / heap
corruption -- EXC_BAD_ACCESS and libmalloc "corruption of free block"
aborts, always with the "cgal_fix_model" worker on the stack inside
add_object_mesh -> Model::add_object / delete_object.
Wrap the repair in a SaveObjectGaurd so the backup manager ignores the
object for the duration of the repair; a single backup is taken when the
guard is released on the main thread after the worker joins. This mirrors
existing batch-edit usage of SaveObjectGaurd (Model.hpp, GUI_ObjectList).
Repro: repair a multi-part / splittable object with auto-backup enabled
(Preferences > Backup); crashed within a few repairs on macOS arm64.
* Update FixModelByCgal.cpp
---------
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
* build: add build_win.bat, a Windows build script for deps, slicer and toolchain setup
build_release_vs.bat takes no options: what it builds is decided by editing
it. This adds build_win.bat alongside it, with short and long options, a
grouped help message, a dry-run mode that prints every command instead of
running it, and one option per thing a developer actually varies - the
configuration, the architecture, the compiler, the generator, the Visual
Studio release, how much gets rebuilt, and where the dependency tree lives.
It works from any directory, needs no developer command prompt in either
generator mode, and keeps CMake ahead of Strawberry Perl on PATH so a build
does not depend on how the user ordered their environment.
scripts/test_build_win.ps1 covers it with table-driven cases that run the
script under --dry-run and assert on the commands it prints, so nothing is
configured or built. The Windows build jobs wait on that suite.
Based on the script from OrcaSlicer#11097.
Co-authored-by: Ocraftyone <24759591+Ocraftyone@users.noreply.github.com>
* build: report what build_win.bat produced and what to do next
Every successful run now ends with a block naming what it built and the
commands to carry on with. Those commands repeat the flags that reproduce
the run, so a rebuild after a clang-cl Ninja build is not silently an MSVC
one. A failure gets a framed block naming the command that failed and a
retry scoped to the stage that failed, so a slicer error does not suggest
discarding an untouched dependency tree.
Installing is now opt-in behind -i. The install tree is a second full copy
of the build that exists mainly so the release can be zipped from it, while
the build tree is already runnable, with the DLLs beside the binary and
resources symlinked rather than copied.
Configuring against a dependency tree that was never built now names it
instead of failing several hundred lines into CMake's package resolution.
scripts/test_build_win.ps1 covers all of it, and gains -Name so one case
can be run without the full pass.
* build: let the test options stand alone, and say which build things apply to
--run-tests named two things to do and then did neither without -s, so
`build_win.bat -lx --run-tests` answered "Nothing to do". Both test
options now imply the slicer build they cannot happen without, unless
another action was already named, so -d --tests is still a dependency
build. --install-vs has turned on --install-deps the same way all along.
That makes them actions, so they move to the group that says so. -i goes
the other way, to the step toggles beside --no-configure and --no-gettext,
since it does not stand alone and adds a step rather than describing what
kind of build to make. An example shows the tests run with toolchain
flags, because flags pick which build gets tested and a bare --run-tests
would build and test a default tree the developer never asked for.
Two help lines named defaults that were not the defaults. --build-dir said
"instead of build/" and --deps-dir said "instead of deps/", but trees are
named for the configuration, compiler and architecture, so build/ is only
the default for a release x64 MSVC build, and deps/ is the source
directory rather than a tree anything is built in.
The hint for a missing dependency tree now carries the flags that
reproduce the run. It said "Build them with -d", which after a clang build
points at the MSVC tree, so following it left you no better off. Every
other suggestion the script makes already repeats them.
-k counted on the developer to read taskkill invocations as progress. It
now names each image and how many processes it is about to stop, which is
what explains the pause, and skips the ones that are not running instead
of printing taskkill's "not found" as though something had gone wrong. No
image can stop the rest.
The environment example set SLIC3R_ASAN, which -a already does, teaching
the long way round to a flag the script owns. It now sets options that
have no flag. The note under it said "Use these for a value containing
spaces. Ampersands are not supported", which named neither what "these"
were an alternative to nor where ampersands were a problem.
The test harness gains a NotExists field, because output cannot show what
a run did not create, and two cases needed to prove exactly that.
---------
Co-authored-by: Ocraftyone <24759591+Ocraftyone@users.noreply.github.com>
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
* ci: build the Windows x64 dependencies and slicer with clang-cl
Passes -l -x from the Windows jobs, using the clang-cl and Ninja options
added in #15373. Ninja runs each dependency's build step as a plain command,
so the jobs set up a VC environment for OpenSSL's nmake. arm64 stays on MSVC
for now.
The deps cache key gains the compiler, so windows-x64 becomes
windows-x64-clang and windows-arm64 becomes windows-arm64-msvc.
* deps: select OpenSSL's ARM64 target from DEPS_ARCH
CMAKE_GENERATOR_PLATFORM is only set by -A, which Ninja never receives, so
the Ninja build selected the x64 target on ARM64. DEPS_ARCH is derived from
CMAKE_SYSTEM_PROCESSOR and is already independent of the generator.
* ci: build the Windows ARM64 dependencies and slicer with clang-cl
Three dependencies need handling first. libpng and OpenCV each build an ARM
SIMD path that does not compile with clang-cl, so those paths are off; PNG
already had the same opt-out for Apple ARM. OCCT is built with cl, since
clang-cl cannot emit one of its large generated files and there is no option
to turn that off. All three are gated to Windows ARM64 with clang.
The rows, their ✗ buttons and the click-to-highlight were all built against
m_doc.features[m_constrain_feat].entity_constraints — a COMMITTED feature. A live sketch
has no committed feature, so the card was hidden for the whole session and the list it
would have shown was empty by construction. Every constraint applied while drawing was
nameless: the badge said one existed, nothing said which.
rebuild_constraint_list now picks its source by scope. live_constraint_scope() is the same
discriminator apply_constraint already used to route to apply_live_constraint — both
Constrain modes set m_active, so is_sketching() alone would claim the live scope while the
committed manager is open. delete_constraint and highlight_constraint_entities branch on
it too, and the card shows in Sketch mode as well as Constrain.
Keeping the rows in step needed a signal that did not exist: on_solve_state fires on every
frame of a drag, so rebuilding from it would rebuild the list continuously. The tool now
fires on_constraints_changed only when the constraint SET changes — one added by
try_add_constraints, one removed by remove_constraint (the indexed form the badge click and
the ✗ row now share).
The rebuild is deferred through CallAfter. One of its callers is the ✗ button's own click
handler, and rebuild_constraint_list destroys those buttons: deleting the window whose
handler is still on the stack is a use-after-free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
A glyph reads as decoration until something says otherwise, so the badges shipped
last commit were discoverable only by accident. Two places now say it, chosen because
they are where the eye already is:
- the moment of applying, which is the one the user is watching ("Applied constraint ·
its badge is on the sketch — click the badge to remove it"). The hint line could not
carry this alone: it only refreshes when the (mode, step, picks) tuple changes, and
applying a constraint changes none of them.
- the Select-mode hint line, appended only while the live sketch actually holds a
constraint, so it never advertises a badge that is not on screen.
Also fixes what the previous commit got wrong: remove_constraint_near solved through
solve_sketch_entities directly, which relaxes the geometry but leaves m_dof and the
per-entity conflict flags untouched and never fires on_solve_state. Deleting a
constraint therefore left the DoF readout describing the system as it was BEFORE the
deletion, and any red over-constrained tint stranded on screen. It goes through
resolve_live() now, the same path every other live edit uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
The constraint badges existed and had never once been drawn where they were needed.
build_constraint_glyphs read m_constrain_cons, a vector only the COMMITTED-feature
Constrain mode fills, and the draw call sat inside `if (m_mode == Mode::Constrain)`.
Every constraint applied during a live sketch — which is the path the Constrain buttons
take while drawing, the one added in "Constrain while you sketch" — went into
m_constraints and was rendered by nothing. You could not see that Parallel had applied,
so "nothing happens" was indistinguishable from "applied and invisible".
The glyph builder now takes its constraint list as a parameter: Constrain mode passes
m_constrain_cons as before, the live session passes its own m_constraints. Same glyphs,
same teal.
Seeing them is half of it. A constraint's entire state is exists / does not exist, so the
toggle is a delete, and there was no way to reach one during a session — the ✗ rows in
the panel list are bound to the committed feature. Each badge now records where it landed
(m_glyph_hits) and a plain left click in Select mode within its cell drops that constraint
and re-solves. Shift/Ctrl clicks are left alone so multi-select still works.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Two reports, one root: the sketch tab could not show what was selected.
The bed grid landed last commit, and selection was painted pure white — a freshly
drawn line is auto-selected by the creation tool, so the first thing a new line did
was disappear into the grid. Selection now wears design_selection_color(), the same
cyan a picked solid already wears. White is kept for the hover handle alone.
That invisibility is also why "I apply Parallel and NOTHING HAPPENS": drawing two
lines leaves exactly ONE selected (the last), Parallel needs two, so the planner
correctly refused — but the status text named only the requirement, never the current
pick, which reads as a dead button. It now reports how many are selected and how to
pick the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
In CAD the centre IS the sketch origin -- GLCanvas3D already moves the axis
triad there for exactly that reason. The grid under it did not agree: it comes
from PartPlate::calc_gridlines, generated from m_origin, the plate's front-left
corner, with an adaptive step meant for a print bed.
Measured on a screenshot from the user's machine: the nearest grid line was 10 px
from the origin in a 23 px pitch. The origin floated mid-cell, in both axes.
Corner-origin is CORRECT for Prepare -- a print bed starts at a corner -- and the
plate list is SHARED with the plater, so re-centring it there would change the
bed for every user of the app to fix one tab. The seam used instead already
existed: _render_platelist takes show_grid, and m_axes_at_bed_center is already
the "this is the Design canvas" flag. The Design canvas suppresses the plate's
grid and draws its own.
Minor every 10 mm, major every 50 mm, both generated from bed_center() so a line
passes exactly THROUGH the origin in each axis. Two GLModels, rebuilt only when
the bed shape changes, not per frame.
White majors, grey minors, the SAME in both themes. There is no white bed to
vanish against: the plate is dark grey either way (DEFAULT_MODEL_COLOR
{0.326,0.337,0.337} light, DEFAULT_MODEL_COLOR_DARK {0.255,0.255,0.283} dark),
a difference of 0.07. An earlier draft inverted the palette on the light theme;
that was a branch buying nothing. For contrast with what this replaces: the
plate's grid draws BOTH its thin and bold families in one 0.43 grey, which is
most of why the stock grid reads as a flat mesh with no scale to it.
z = -0.26, the same value as PartPlate::GROUND_Z_GRIDLINE -- below the bed fill
at -0.03, above the bed model at -0.41, so no z-fighting. Matched by
construction, since that constant is file-static in another TU.
Known limit, commented: the grid is clipped to the bed's BOUNDING BOX, not its
polygon. Identical on a rectangular bed; on a circular one it would spill past
the round edge. The target printers are rectangular.
Verified on the rig, not just compiled: white majors over a fine grey mesh, and
a white line through the origin in both axes.
snaporca-kha0
Twenty buttons on the CONSTRAIN bar, nine of them exercised. The other eleven
were "implemented" in the sense that the kernel builds the right def for them --
which is exactly what was true of Parallel yesterday morning, right up until a
user pressed it and got nothing.
What a kernel test cannot see is whether the BUTTON is wired to the index its
name claims. CON_BTN is 449 + 42*i over a hand-written name list, and it has
drifted once already: six buttons were inserted, everything from index 6 on
pointed at the wrong control, and nothing caught it for months because no rung
pressed past index 5. D13 presses index 6; D14 through D22 press 8 to 19. The
map turned out to be intact, which is worth knowing rather than assuming.
D12 vertical, D13 equal_radius (its own button, not Equal's promotion),
D14 concentric, D15 tangent, D16 midpoint, D17 symmetric (the three-pick form),
D18 sym_h, D19 radius, D20 diameter, D21 fix, D22 dist_y.
Three are shaped around a specific way the code could be wrong rather than
around "does something happen":
D20 exists for a factor of two. Diameter wired to the Radius handler gives
r = 30 for a typed 30, and nothing on screen looks wrong.
D21 -- Fix alone is unfalsifiable: nothing moved, so nothing proves the
constraint exists. It only becomes observable when a SECOND constraint would
otherwise move the fixed point, so the rung drives the pair to a 70 mm gap and
checks which end travels.
D14 asserts the radii did NOT change. Concentric is about centres; a solve
that also equalised the radii would pass a naive check.
D17 failed on its first run and the rung was wrong, not the app: all three
entities are free, so the solver is entitled to satisfy the mirror by moving the
AXIS instead of the points -- and it did, landing the pair symmetric about
x = -8.18. It now measures signed perpendicular distance to the axis where the
axis actually is, which is the stronger property anyway.
Coverage: 20/20 buttons pressed, up from 9. Ladder 135 -> 177 properties across
43 rungs, all holding.
snaporca-l2vm
The FFmpeg camera view port made pkg-config a required build tool on
Windows. Windows does not ship one, so every Windows developer has to
install it before the build will configure:
Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)
Call Stack (most recent call first):
CMakeLists.txt:480 (find_package)
Nothing on Windows needs it. FFmpeg there is a prebuilt zip unpacked
into the deps prefix, whose DLLs the top level CMakeLists already names
by exact soname. The version is fixed before configure runs, so
find_library against that prefix does the job, as on macOS.
Also drops the CI step that installed pkg-config, gated on !SELF_HOSTED
so it never ran on self-hosted runners, and re-comments the if(WIN32)
block that #15234 uncommented only for that find_package.
Five defects from ten minutes of real use, and the mode split behind the worst
of them. One commit because the changes overlap in the same functions; the
pieces are separable in the diff, not in the file.
CONSTRAINING NO LONGER NEEDS A COMMITTED SKETCH. A constraint could only be
applied by committing the sketch, selecting it in the feature tree, pressing the
padlock, and only then picking. While drawing, the CONSTRAIN toolbar was not even
on screen (set_ui_mode showed it in UiMode::Constrain alone) and apply_constraint
answered "Press Constrain on a sketch first" -- in a status line nobody looks at.
Draw two lines, press Parallel, get nothing: that is what a user reported as "the
UX is a mess", and they were right.
apply_constraint now takes the live session first, reading the picks from the
selection model the sketch tool already had (click, ctrl-click to extend,
double-click for the loop) and applying through try_add_constraints, which
already did append -> solve -> keep-or-rollback. The committed Constrain path
stays for editing an old sketch; it is no longer the only way in. The twenty
constraint buttons now show in Sketch mode as well.
The discriminator is is_sketching() && !is_constraining() &&
!is_constraining_entities(). Both begin_constrain and begin_constrain_entities
set m_active, so is_sketching() alone is true DURING a constrain session and the
new path would hijack the old one -- compiling perfectly and failing in
behaviour.
ONE PLANNER, NOT TWO. A second caller meant duplicating the logic that decides
whether a constraint is legal, which roles it binds and whether it needs a typed
value. That duplication is how today's Coincident bug survived: fixed in one
branch, alive in the next one down. plan_entity_constraint() now lives in the
kernel -- pure, no wx, no translation -- and both UI paths call it. DesignPanel
loses 304 lines and gains 155.
Being in the kernel makes it TESTABLE. The Parallel defect existed because a
constraint type met an entity type nobody had tried, and the only instrument was
a 13-minute GUI ladder. 19 new kernel cases cover the matrix: 264 -> 283 cases,
7648 -> 7867 assertions.
Parallel, Perpendicular and EqualLength gain the two-line guard they never had.
On non-lines they used to emit a def the solver silently dropped -- the sketch
reported itself constrained when it was not, the same class as Horizontal on a
Point. EqualLength on two rounds still promotes to EqualRadius first.
Symmetric is planned completely, including its axis pick: the plan carries a
VECTOR of defs because Symmetric on two lines is two constraints (P0/P0 and
P1/P1). A single def would have half-applied it -- one end pinned, one free,
looking correct until something moves.
A PLACED POINT SURVIVES THE COMMIT. Type::Point was created correctly and never
drawn once committed: both renderers skip it, correctly, since entity_polyline
gives a point nothing. What was missing is the vertex-marker path the live
session already used. rung_point passed throughout because it asserts the
document, and the point was always in the document -- the pixels lied.
ESC STOPS EATING AN UNSAVED SKETCH. The third press reached cancel_sketch(),
clearing m_entities with no warning and nothing to undo. live_sketch_has_work()
existed and was never consulted. The exit layer refuses once when there is work
and lets a second consecutive Esc through; the refusal re-arms on a button press,
never on mouse motion, or Esc could never exit while the hand moves.
TWO NEW RUNGS. D10 drives Parallel through the committed path -- it passes on
the PRE-fix binary, which is how we know the user's failure was the mode and not
the constraint. D11 is the acceptance for the collapse: draw, pick both, press
Parallel, no commit and no padlock. Ladder 126 -> 135 properties, all holding.
CON_BTN_SKETCH is measured, not derived: in Sketch mode the group renders after
the sketch toolbar, so the first button is at 677, not 449. Pitch 42, twenty
buttons, read off a screenshot. Deriving it by offset is how that table drifted
the last time.
Known limit, commented at the call site: a constraint added to a LIVE sketch is
not on the document undo stack, so Ctrl+Z will not take it back until the sketch
is committed.
snaporca-itp4, snaporca-oyhx, snaporca-l2vm
PartPlate::store_to_3mf_structure read first_layer_time from the indirect cali_bboxes_data struct,
which the GUI populates at Plater.cpp:10600 but the CLI never writes to. The result was uninitialized
memory leaking into slice_info.config
Read directly from get_slice_result()->initial_layer_time, which is populated by
GCodeProcessor::finalize() in both code paths and matches the pattern already used a few lines
above for gcode_prediction.
Also default-initialize PlateBBoxData::first_layer_time to 0.0f as a defense against any other consumer
reading it without an explicit write.
build: clear 54 dead private fields
54 of the 161 -Wunused-private-field warnings, across 31 files. These are
the ones needing no judgment. Each member is declared once and appears
nowhere else in src/, counting the .mm and .c sources as well as .cpp and
.hpp, so nothing writes them and nothing reads them. Every removal is a
whole line, and no declaration shares a line with another member.
The remaining 107 are left alone. Those members are mentioned elsewhere,
usually assigned and never read, where the fix might be deleting the
member or might be restoring a read that went missing.
build: clear 53 unused value warnings
49 of them are deliberate i18n markers. L(s) expands to s, so
L("Main Extruder"); is a string literal as a statement and its value is
discarded. The strings have to stay, because the real values come from
printers/*.json at runtime and xgettext cannot scan those. Each block is
now a static const char *const markers[], which uses the values rather
than discarding them. Extraction is unchanged: the xgettext invocation
from scripts/run_gettext.bat gives 76 msgids over the two marker files
before and after, with identical msgid and msgctxt sets.
The other 4 are statements with no effect. AMSItem.cpp:117 and :174
construct and drop a wxColour(255, 255, 255); AMS_TRAY_DEFAULT_COL is
that colour, and the line above already assigns it. UpgradePanel.cpp:865
reads a member and drops it.
wgtDeviceNozzleSelect.cpp:269 writes
if (item; auto ptr = m_nozzle_rack.lock()), which puts the null check in
the init-statement position where its value is discarded, so the check
never runs, and sGetNozzlePosId then dereferences item. Nothing reaches
that today, because the only sender of the event sets itself as the
event object and the dynamic_cast always succeeds. The check now runs.
git config wrote core.autocrlf into the repository git init creates in the
extracted CPython source. Nothing outside deps/build reads that repository, so
the setting was already contained.
-c applies the override to the one invocation instead, so no repository config is
written at all. It cannot reuse PATCH_CMD, so the shared flags are spelled out here.
Port of snaporca da8d011b87; parity holds (DesignPanel.cpp still exactly 32 divergent
lines). Verified independently on this fork's own rig: full ladder 126/126 against
BuildID 96c697a3, built from this tree.
Reviewing the DistanceX/Y fix for OTHER members of its class found three more live defects
on the constrain toolbar. All four share one root: a branch assumes every picked entity has
two endpoints, and the solver's refusal to resolve a role it cannot find is silent.
COINCIDENT had the identical closest-pair walk over {P0,p0},{P1,p1}. For two Points the
phantom (0,0) pair sits at distance 0, which is the smallest distance there is, so it
ALWAYS won: ptOf(Point,P1) -> 0, ref_ok fails (SketchSolver.cpp:185), constraint dropped.
Not sometimes -- every press.
HORIZONTAL/VERTICAL hardcoded ra=P0, rb=P1 with no type check. With a Point picked the
constraint is dropped by the same mechanism but still STORED: constraints goes 0 -> 1 after
the commit and nothing moves, so the Constraints list shows a dimension that can never do
anything. Worse than refusing -- the panel claims the sketch is constrained when it is not.
ANGLE computed p1-p0 on whatever was picked. On a circle that is (0,0)-centre, so two
circles pre-filled the field with the angle between their centre POSITION VECTORS (178.83
deg for two on the x axis), and accepting it emits SLVS_C_ANGLE on two circle prims.
Both branches now refuse with a message. entity_ends()/closest_ends() are file-scope and
shared by Coincident and DistanceX/Y, so there is one implementation instead of two that
drift.
Two smaller findings from the same review: infer_auto_constraints' roles_of omitted
EllipseArc while heal_coincidences' identical copy has it; and set_point(Circle, Center)
wrote e.center and not e.p0, breaking the "p0 mirrors centre" invariant for the duration of
a live drag.
New rungs D8 and D9, both RED against the shipped binary and green here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Format/3mf.cpp also saved and loaded it, but nothing calls its store_3mf and
its load_3mf only sees files fingerprinted as PrusaSlicer's, which never carry
a recipe. Its round-trip test only exercised that dead loop. The BBS backend,
which every save and load goes through, is untouched.
The Design tab's viewport is the fourth GLCanvas3D on the shared GL context and
the only one the plater does not own, so unbind_canvas_event_handlers() and
reset_canvas_volumes() never reached it — the macOS Command+Q and Debian cases
those calls exist for. Its frame-level handlers become members so they can be
unbound.
The bind sat above GLCanvas3D::bind_event_handlers(), and on_size never Skips,
so wx's reverse-order dispatch stopped before it and the handler never ran.
Port of snaporca b3221f8a12; this is the fork the fault surfaced on.
The perpendicular rung drew its two lines 93.5 degrees apart and then asserted they did
NOT start perpendicular. On this rig, whose camera maps the same click a pixel differently,
they arrived at exactly 90.000000 -- inference had already done the job the rung exists to
test, so the precondition failed while every later check passed. Held on the other fork and
failed here from identical source: the rung depended on where a click happened to land, not
on the app.
The second point now starts the pair 56 degrees off, well outside any snap tolerance, so
the button has real work to do.
That immediately exposed a second, milder fault in the same rung. From a 51 degree start
the LIVE solve converges to its own tolerance and lands at 89.999999991; the old 1e-9
assertion held only because the correction used to be tiny -- it was measuring how little
work the solver had to do, not whether the lines came out perpendicular. It is 1e-6 degrees
now, which is 1.7e-8 radians. The round-trip check still demands exactly 90 and gets it,
because the committed feature re-solves from scratch.
Full ladder 118/118 on BOTH rigs after this, each driving its own fork's binary. This fork
had never had a green gesture ladder before today (snaporca-eoj1).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Four of this fork's five absolute chrome coordinates were shifted by CHROME_DY when the
ladder was first brought up here (DESIGN_TAB, CONSTRUCTION_CHECKBOX, CON_BTN_Y,
CONFIRM_BTN). TREE_ROW0 was not, because it is declared above the CHROME_DY block and was
simply never in view.
The unshifted click lands 26 px below the first tree row, just past its 23 px height, so
the row is never selected and Delete does nothing. reset_document then spends 40 rounds on
it and dies with "could not empty the feature tree" — a message that names the feature
tree, which is not the fault. The same 26 px is why confirm_and_reopen's double-click did
not reopen the sketch, which surfaced as "sketch_describe: no sketch is open" three frames
away from the cause.
Measured, not inferred: the Sketch1 row centre reads y=241 on the rig at 1920x1080 with
the window at (0,0), against the constant's 215. CONFIRM_BTN was checked in the same pass
from a screenshot taken in CONSTRAIN mode and is correct at (1751, 101).
With this, the four new constraint rungs hold 20/20 on this fork's rig, driving the binary
built from 648b930e75 (BuildID 56417445) — so the DistanceX/Y fix ported here is now
exercised, not merely parity-checked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Port of snaporca 2951e3c60b; parity holds (17 identical, DesignPanel.cpp still exactly 32
divergent lines, so the hunk landed on the right side of the DropDown divergence).
The sketch-constraint epic added six toolbar buttons and covered all six with kernel
tests. Not one of them was ever clicked. The gesture ladder only pressed 'perpendicular'
and 'equal' -- and CON_BTN, which locates buttons by index, was silently wrong for every
entry past index 5 for the whole epic. The untested half of the toolbar was exactly the
broken half (snaporca-rqsy).
Four rungs now drive them: D4 Equal on two circles (must mean equal RADIUS, not the
equal-length no-op the epic fixed), D5 Collinear on two oblique lines, D6 a horizontal
distance, D7 symmetric about the implicit vertical axis. Full ladder 118/118 on snaporca;
this fork's rig has not been rebuilt against the change yet, so here it is reviewed,
parity-checked and NOT exercised.
D6 found a shipped defect. apply_entity_constraint enumerated {P0,p0},{P1,p1} for BOTH
entities regardless of type, but a Point's p1 is unused and reads (0,0), as does a
Circle's. The closest-pair search then picked those two phantom origins, distance 0: the
field opened pre-filled 0.00 and the solver dropped the constraint, because ptOf(Point,P1)
resolves to no handle. Nothing errored -- the dimension simply did nothing. ends_of() now
enumerates only the roles an entity actually exposes, and the pair with no point at all is
refused with a message instead of a silent no-op.
Five kernel tests, a 7/7 ladder, a review and a fork port all passed over this, because
every one of them exercises the kernel, where the geometry was always right.
Two rig faults fixed in the same pass, both of which produce a green-looking session that
tests nothing: start-headless-gui.sh never exported SNAPORCA_MCP or SNAPORCA_KEYTRACE, so
a freshly launched rig comes up healthy and every ladder dies on "Connection refused"; and
it never dismissed the "Restore" dialog a killed session leaves behind, which grabs every
synthetic click afterwards.
The value field also takes no keyboard focus from the WM -- typed digits go to the canvas
and Return commits the pre-filled number (typed 40, got 54.94). focus_field() finds it as
its own top-level window and clicks it first. The no-op tolerance is now 5e-3, the field's
own two-decimal display resolution, not 1e-6.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
These dialogs treated a filament with no compatible_printers as compatible with
nothing, while the rest of the app treats it as compatible with everything, so
the entire Orca Filament Library was missing from the AMS material and
calibration filament lists. They now resolve compatibility the same way the
plater does, and a vendor profile still supersedes the library generic of the
same name.
This fork had never been gated end to end. Five separate things stopped it, none
of them a defect in the CAD code itself, and each failure named the wrong
subsystem — which is why they survived.
1. run-all-checks.sh invoked docs/ux/mockups/gen_offer_table.py. The design docs
moved to docs/CAD/ (bbd1989e1e) and this path did not follow, so the rung
failed on a missing file.
2. gen_offer_table.py then resolved REPO one dirname short, because it now sits a
level deeper. OUT pointed at docs/src/.../DesignOffer.hpp, which does not
exist, so --check diffed the generated table against an EMPTY file and
reported all 189 lines as a difference.
3. DISPLAY was never passed into the container. The check scripts fall back to
":10", which is the other fork's rig; this one's Xvfb is :11. Symptom:
"FATAL no app window on :10", which reads like a dead app.
4. Every absolute chrome coordinate was written in the Snapmaker fork's layout.
This fork keeps mainline's top row (File / save / undo / redo / Calibration
with the title), so the whole chrome sits 26 px lower. At the unshifted y the
Design-tab click landed in the toolbar and the app stayed on the Home page,
reported as "no sketch opened after plane click + Shift+S"; the unshifted
Construction checkbox reported "0 construction axis". The three constants now
derive from CHROME_DY. Canvas coordinates were never affected -- clickmm()
computes them from live canvas geometry -- which is why dozens of geometric
properties passed exactly on a GUI that had never been driven.
5. CON_BTN was stale for every index after 5, from the sketch epic's six new
buttons. Fixed in both forks; see the companion commit on snaporca.
pdftocairo was also missing from the rig image (installed there, not a repo
change), without which every corpus sheet threw.
RESULT: offer-table, kernel, engine, corpus, corpus-scale and offer all hold.
The gesture ladder now reaches D2 and applies Equal length through the Constrain
toolbar, both sides landing at 99.928133658; it then fails re-entering the sketch
after commit, filed as snaporca-eoj1 with the evidence and the next measurement
to take. Kernel here is 7701 assertions / 270 cases.
# Description
This is an initial draft of the plugin audit workflow.
It focuses on the user experience and developer-facing permission
workflow. It does not yet include the complete implementation of every
operation that should be audited, such as the full filesystem,
networking, and process-spawning event coverage.
## User workflow
When a plugin is loaded:
1. The plugin’s register_capabilities() function is executed.
2. The plugin declares the permissions it requires.
3. OrcaSlicer displays a permission dialog listing the requested
resources.
4. If the user grants access:
- The permission is persisted in the plugin’s .install_state.json.
- Capability registration continues.
- The plugin is materialized and loaded.
5. If the user denies access:
- Plugin loading fails before capabilities are materialized.
- on_load() is not called.
- The plugin’s install state is marked with "enabled": false to prevent
repeated automatic load attempts.
At runtime, if a plugin accesses a resource that was not approved during
loading, the audit hook displays another permission dialog. For
filesystem requests, the dialog identifies the requested filepath.
- Granting access persists the permission and allows the operation.
- Denying access raises a Python PermissionError.
- The error propagates to the host, which records the failure and
unloads the plugin.
Host-side traceback logging is performed outside the plugin audit
context so that logging does not generate additional permission dialogs.
## Developer-facing API
Plugins can declare filesystem read permissions through the new API:
```python
import orca
AUDIT_PATH = __file__
@orca.plugin
class ExamplePackage(orca.base):
def register_capabilities(self):
orca.request_permissions(
fs_read=[AUDIT_PATH],
)
orca.register_capability(ExampleCapability)
```
orca.request_permissions() must be called from register_capabilities()
while the plugin is being loaded.
Currently supported permission:
orca.request_permissions(fs_read=[...])
The paths should be explicit filesystem paths that the plugin intends to
read. The host deduplicates repeated paths, presents the request after
registration completes, and persists granted paths in the plugin
install-state sidecar. This API is still experimental, and is by no
means the final implementation.
Support for additional permission categories, including filesystem write
access, networking, and process spawning, is reserved for subsequent
work.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
<img width="869" height="799" alt="image"
src="https://github.com/user-attachments/assets/8a5903cc-0cbb-45a8-b88a-706d6cba790f"
/>
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
This fork's kernel suite has never run. scripts/CAD/run-kernel-tests.sh died at
CMake CONFIGURE time on find_package(assimp REQUIRED), before a single source
file compiled, so every kernel change ported here was parity-checked against
snaporca and never independently tested (snaporca-w80c).
WHY IT WAS MISSING. OrcaSlicer mainline gained assimp (glTF/GLB/FBX import for
texture-to-colour) after the orcacad-deps image was baked: the image's deps/ tree
has no Assimp directory at all and nothing named assimp anywhere in it. On the
host, deps/build/dep_Assimp-prefix carries only `patch` and `update` stamps -- no
build, no install -- so the dependency was fetched and then never built, inside
the image or out of it. The Snapmaker fork never hit this because its base
requires neither assimp nor OpenCV.
WHY A LAYER. A full deps rebuild is hours and would rewrite artifacts that
currently work; this adds the one missing package on top. It is a Dockerfile
rather than a `docker commit` so that what was done stays reviewable and
repeatable instead of being an undocumented image mutation.
The flags are the project's own recipe (deps/Assimp/Assimp.cmake) plus the
standard superbuild arguments from orcaslicer_add_cmake_project
(deps/CMakeLists.txt:158) and DEP_CMAKE_OPTS (deps/deps-linux.cmake). The file
says to keep them in step with that recipe: it stands in for the superbuild, it
is not a separate opinion about how to build assimp.
The tarball's SHA256 was checked against the recipe's URL_HASH before this was
written and is re-checked inside the build, and the build asserts the installed
cmake config exists rather than trusting an exit code. OpenCV was confirmed
already present, so it is not a second wall behind this one.
RESULT, and it is the point: orca_cad kernel now runs and is GREEN at 7701
assertions / 270 cases. snaporca is 7700 / 270 -- same cases, one more assertion
here, which is the tolerated test_caddocument.cpp divergence. The "this fork's
kernel suite cannot run" caveat carried by the five commits of the sketch
constraint epic no longer applies.
Port of snaporca e635627b81. Parity OK: 17 files identical, 8 diverging at their
expected counts.
The last reference an industrial sketcher offers that this one did not: Onshape's
Use, SolidWorks' Convert Entities. Project already turned a body's 3D edges into
2D Lines, Circles and Arcs on a plane, but the result landed in its OWN feature,
so while drawing in one sketch you could not borrow an existing body's edge and
constrain to it.
No new geometry code: Project's per-edge conversion loop is factored into
project_edges_to_entities() and called from a second entry point that appends
into an EXISTING sketch with construction = true. The loop appears once now,
not twice.
Construction is what makes it cheap and safe: SketchEngine already skips
construction entities when building wires, so the references guide without being
built, and being otherwise ordinary entities every constraint from this epic --
Collinear, EqualRadius, the axis-projected distances, PointOnLine, Symmetric --
works against them for free.
Part of this refactors working code, so Project's behaviour identity is the
invariant; the existing [CadDocument][project] cases guard it and a new case
asserts a Project feature still emits construction == false.
project_edges_into_sketch returns the number of entities appended, or -1 on a bad
reference rather than throwing.
VERIFICATION LIMIT, as with the previous four commits: this fork's kernel suite
still cannot run (find_package(assimp) at configure time, snaporca-w80c). Shared
sources are byte-identical to snaporca's, where kernel is 7700 assertions / 270
cases and ALL LADDERS HELD 7/7.
Port of snaporca 2d36d28770. Parity OK: 17 files identical, 8 diverging at their
expected counts.
infer_axis_constraint returned only Horizontal or Vertical. On the
CAD-1000-hours corpus the top two transitions are sketch_dim -> sketch_draw
(5896) and back (5756): the signature of geometry that does not self-constrain
as it is drawn.
Every rule requires the relation to be ALREADY TRUE within tolerance, so nothing
the user drew is moved; parallel/perpendicular and tangent additionally require a
shared endpoint.
TWO LIMITS THE CORPUS RUNG FORCED, neither visible to the unit tests:
1. At most ONE constraint per rule per new entity, not one per PAIR, and no
one-at-a-time fallback for the relations batch. EqualRadius has no locality
restriction, so 200 equal holes produced ~20000 candidates; the rejected batch
then cost a solve per constraint and pinned the app at 95% of a core with the
MCP socket unresponsive.
2. Relations only for gesture-sized batches. "A scripted add is not a drawn
gesture" is already this file's rule at its bulk call site (snaporca-8xg1), and
EqualRadius also couples geometrically distant entities, merging independent
connected components and defeating the partitioning that makes large sketches
solvable (snaporca-yww4). With the cap alone geometry stayed correct (32/32
sheets clean) but seven of the largest timed out, including MPD681 -- the sheet
that call site's own comment names.
Also fixes the tolerance leak behind 2: the bulk path asks for exact inference
with ang_tol_rad = 0 but len_tol_frac kept its 0.01 default.
ALSO independent of this feature: run-kernel-tests.sh defaulted to
TAGS=[CadDocument] while four CAD test files carry their own tags and nothing
selected them (2624 assertions / 206 cases reported, 7648 / 264 actual). All 58
dark cases were passing; the coverage was never exercised.
VERIFICATION LIMIT, as with the previous three commits: this fork's kernel suite
still cannot run (find_package(assimp) at configure time, snaporca-w80c). Shared
sources are byte-identical to snaporca's, where kernel is 7651 assertions / 265
cases and ALL LADDERS HELD 7/7.
Port of snaporca 5b1294de59. Parity OK: 17 files identical, 8 diverging at their
expected counts.
Every industrial sketcher gives you the origin and the axes as references. Here
the origin was only a SNAP target and the axes did not exist, so Symmetric needed
a third picked ENTITY as its mirror axis: symmetry about the sketch's vertical
axis first required drawing a construction line.
Every constraint reference resolves through four lambdas in the solver
(valid/ptOf/primOf/coordOf), so teaching those about three negative sentinel
indices makes the origin and both axes available to EVERY constraint type at
once. No new SketchEntity type, no serialization change; -1 still means "unset".
The references live in G_FIXED and add no degrees of freedom, which a test
asserts via the reported DoF.
SymmetricAboutY / SymmetricAboutX are two buttons that need no third pick and no
construction line. Making the axes clickable in the viewport is deliberately left
out: that is canvas hit-testing work with its own risks.
Recorded in the tests because it will catch the next person: sys.dragged[] is
populated only during a drag, so a plain sketch_solve of an UNDER-constrained
system may move any free parameter -- solvespace runs Newton, it does not
minimise movement. PointOnLine onto an axis is one equation in two unknowns and
the point legitimately slides along it. Those tests pin the free direction
instead of asserting the other coordinate is untouched.
VERIFICATION LIMIT, as with the previous two commits: this fork's kernel suite
still cannot run (find_package(assimp) fails at configure, snaporca-w80c). The
shared sources are byte-identical to snaporca's, where kernel is 2624 assertions
/ 206 cases and ALL LADDERS HELD across all seven rungs.
Port of snaporca 9fa304c77a. Parity OK: 17 files identical, 8 diverging at their
expected counts.
The everyday dimension in SolidWorks and Onshape, and this kernel had no form of
it. Distance constrains the straight-line gap; LockX/LockY pin one point's
ABSOLUTE coordinate. Neither relates two points along an axis.
DistanceX/DistanceY emit SLVS_C_PROJ_PT_DISTANCE against two unit direction
lines built in the solver's G_FIXED group, so they add no degrees of freedom.
THE DIRECTION IS SIGNED, and getting it backwards is silent. libslvs defines a
LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp) and
PROJ_PT_DISTANCE constrains (pB - pA).dot(dir) (constrainteq.cpp:234), so the
reference lines are built head-first to mean +X and +Y.
The same signedness was a real defect in the GUI: the inline editor was
pre-filled with |delta|, so when the closest endpoint pair ran right-to-left,
opening the dimension and accepting the number shown would flip the point to the
other side of its anchor. Opening a dimension and accepting its own value must
be a no-op. The refs are now ordered so the shown value is positive.
On the CAD-1000-hours corpus, dimensioning and constraining is 31.9% of all
observed CAD time -- the largest single class, 7.6x feature operations. This is
the item in the constraint epic that lands most directly on it.
VERIFICATION LIMIT, as with the previous commit: this fork's kernel suite still
cannot run (find_package(assimp) fails at configure time, snaporca-w80c). The
shared sources are byte-identical to snaporca's, where kernel is 2603 assertions
/ 200 cases and ALL LADDERS HELD across all seven rungs.
Port of snaporca 9ec6405e2d. Parity OK: 17 files identical, 8 diverging at their
expected counts (DesignPanel.cpp 32, test_slvs_constraints.cpp 3).
Measured on the CAD-1000-hours corpus: 51.5% of observed CAD time is 2D sketch
work, and dimensioning/constraining alone is 31.9% -- the largest single class.
Two constraints every industrial sketcher has were missing here.
EqualRadius fixes a dead end rather than adding a feature. Picking two circles
and pressing Equal emitted EqualLength, which maps to SLVS_C_EQUAL_LENGTH_LINES
and constrains nothing on a curve: a silent no-op with no error. Equal is now
one button with two meanings, as in Onshape and SolidWorks.
Collinear emits PARALLEL plus PT_LINE_DISTANCE=0 rather than PT_ON_LINE, whose
internal valP param this libslvs port leaves at 0, drifting an already-collinear
pair.
Both types are appended at the END of SketchConstraintType: cereal serializes it
positionally, so inserting elsewhere reinterprets every saved recipe.
VERIFICATION LIMIT, stated rather than implied: this fork's kernel suite could
NOT be run. scripts/CAD/run-kernel-tests.sh fails at CMake configure time on
find_package(assimp), before any source compiles -- a pre-existing deps gap
(snaporca-w80c), not this change. The shared sources are byte-identical to
snaporca's, where the full gate passed: kernel 2588/195 and ALL LADDERS HELD
across all seven rungs.
Also fixes two defects in this fork's scripts/CAD/run-all-checks.sh:
- `cd $(dirname $0)/..` landed in scripts/ instead of the repo root, so every
rung looked for itself under scripts/scripts/. Broken since the script moved
into scripts/CAD/; the three sibling scripts were fixed then and this was
missed, so the gate has not run since.
- C defaulted to snaporca-gui, the OTHER fork's rig container, so this fork's
gate would drive snaporca's app and report green about the wrong binary.
run-kernel-tests.sh:31 documents the identical defect being fixed once
already for the build volume; this is the third instance.
* Add ffmepg dep
* NEW: reimpl wxMediaCtrl from ffmpeg
Jira: none
Change-Id: I46a47118a7649b2a50fcce8911e2888342ef25de
(cherry picked from commit d6c7f08769c8cfdbbf0e80ad280c9b3408a3c27d)
(cherry picked from commit 94d91be60bfe9bbbcdd21f85b46abc3faf126f17)
* FIX: reset bambu lib after restart network plugin
Change-Id: I4a3a4b7420745835ca3fa00c6edebe9d8d98cbf6
Jira: STUDIO-7571
(cherry picked from commit 28d9c6743fae80bfd40e4ee391e30d62cb16d4ab)
* FIX: ffmpeg decoder memory leak
Change-Id: I997572b5730618a969959f9b24c405d80fa9f83c
Jira: STUDIO-7597
(cherry picked from commit 342cea29bd9593fa89cbb33caff58055b46ebeec)
* FIX: install ffmpeg symbolic sos
Change-Id: Ia4a45182cefcf62a7a4b4a5c89c92251609c5a68
Jira: none
(cherry picked from commit b7f8fa1efdbe0ac2cc896ca24f063f5894fe9f90)
* FIX: ffmpeg swscale & frame_size
Change-Id: I9f4cb8c739b726f7e5cdbe0df7ed06b2eb2154d5
Jira: STUDIO-7624
(cherry picked from commit 5a2c75d835fb437667b590a803eef148baa30875)
* FIX: wxMediaCtrl3 idle image & center pos
Change-Id: Ib9652573e31bfd6229f174c0a1388942d9d98822
Jira: STUDIO-7633
(cherry picked from commit d51247c46e26460b151de79c598d81151280e79c)
* FIX: AVVideoDecoder sws_ctx_ == nullptr on zero size
Change-Id: I9698354bb1f341e276ec9780d4ef4fcd9f8a1028
Jira: STUDIO-7706
(cherry picked from commit ff622e25026a8471c39eb308cf5b115c4a9d84aa)
* fix:cannot open shared object file on linux
Change-Id: Ica66500506cfe8932eac3ae0a58fb7ff30d1da9b
jira:none
(cherry picked from commit febd1aeb4d453bc96571fa5e5727e9e10046cb80)
(cherry picked from commit 5ad579f929154779abd84b01438fd235c647dbf5)
* NEW:add ffmepg build Cmake
buildLinuxImage add ffmpeg so file
jira:nojira
Change-Id: I3e1be53aa58a179b8d9ae048ed7538de3ae8d111
(cherry picked from commit 2d70a1bcb6a5ba601525b08a38e7610f018fe106)
* FIX: ffmpeg cmake install error
jira:nojira
Change-Id: I74cc0f7c86b5364e55cad2af2bd9a82306ee6864
(cherry picked from commit 805df79e3bb044dac29ec1c06736751ccf3675f9)
* FIX: decode video to wxImage on Linux
Change-Id: I5e332a1b0622b3dfc70ac5c4c3bfa62b3411ebdc
Jira: none
(cherry picked from commit c787ba921a31f259e8eb23fd59f178e96279caf9)
* FIX: wxMediaCtrl3 enter Stopped state soon
Change-Id: I120e9d4b9f85599a184650d1d95fe2bec42af171
Jira: STUDIO-8280
(cherry picked from commit 7648d96305d510b9e97f22124961de5115cde830)
* FIX: reset decode buffer zero when scale width changed
Change-Id: Iaa2f99111dd5f7228b7b25e1be0a8cbdbfe982a6
Jira: STUDIO-8422
(cherry picked from commit 659ebc7d07a8f6045ba5443141b44277d7257cec)
* slic3r: Fix missing declarations in wxMediaCtrl3.h
src/slic3r/GUI/wxMediaCtrl3.h:80:10: error: ‘condition_variable’ in namespace ‘std’ does not name a type
80 | std::condition_variable m_cond;
| ^~~~~~~~~~~~~~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::condition_variable’ is defined in header ‘<condition_variable>’; did you forget to ‘#include <condition_variable>’?
26 | #include "Printer/BambuTunnel.h"
+++ |+#include <condition_variable>
27 |
src/slic3r/GUI/wxMediaCtrl3.h:81:10: error: ‘thread’ in namespace ‘std’ does not name a type
81 | std::thread m_thread;
| ^~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::thread’ is defined in header ‘<thread>’; did you forget to ‘#include <thread>’?
26 | #include "Printer/BambuTunnel.h"
+++ |+#include <thread>
27 |
In file included from src/slic3r/GUI/MediaPlayCtrl.h:17,
from src/slic3r/GUI/MediaPlayCtrl.cpp:1:
src/slic3r/GUI/wxMediaCtrl3.h:77:13: error: field ‘m_frame’ has incomplete type ‘wxImage’
77 | wxImage m_frame;
| ^~~~~~~
(cherry picked from commit 727a73333bd67acf5ff2b1c51ff284c2bacdb413)
* slic3r: Fix missing includes in AVVideoDecoder
In file included from src/slic3r/GUI/AVVideoDecoder.cpp:1:
src/slic3r/GUI/AVVideoDecoder.hpp:28:20: error: ‘wxImage’ has not been declared
28 | bool toWxImage(wxImage &image, wxSize const &size);
| ^~~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:28:36: error: ‘wxSize’ has not been declared
28 | bool toWxImage(wxImage &image, wxSize const &size);
| ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:38:10: error: ‘vector’ in namespace ‘std’ does not name a template type
38 | std::vector<uint8_t> bits_;
| ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:9:1: note: ‘std::vector’ is defined in header ‘<vector>’; did you forget to ‘#include <vector>’?
8 | #include <libswscale/swscale.h>
+++ |+#include <vector>
9 | }
src/slic3r/GUI/AVVideoDecoder.cpp:145:89: error: invalid use of incomplete type ‘class wxBitmap’
145 | bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
| ^
(cherry picked from commit 781ce14e061366da64fdc2d0d592fa35ee57e67e)
* slic3r: Fix missing includes in wxMediaCtrl2
src/slic3r/GUI/wxMediaCtrl2.cpp: In lambda function:
src/slic3r/GUI/wxMediaCtrl2.cpp:170:13: error: ‘wxMessageBox’ was not declared in this scope; did you mean ‘wxInfoMessageBox’?
170 | wxMessageBox(_L("Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Bambu Studio?)"), _L("Error"), wxOK);
| ^~~~~~~~~~~~
| wxInfoMessageBox
src/slic3r/GUI/wxMediaCtrl2.cpp: In member function ‘void wxMediaCtrl2::Load(wxURI)’:
src/slic3r/GUI/wxMediaCtrl2.cpp:179:5: error: ‘wxLog’ has not been declared
179 | wxLog::EnableLogging(false);
| ^~~~~
(cherry picked from commit 73908d38d8b1f7c8dcae92d55711bc08cbfff23c)
* slic3r: Fix missing wxPaintDC declaration
src/slic3r/GUI/wxMediaCtrl3.cpp: In member function ‘void wxMediaCtrl3::paintEvent(wxPaintEvent&)’:
src/slic3r/GUI/wxMediaCtrl3.cpp:121:5: error: ‘wxPaintDC’ was not declared in this scope; did you mean ‘wxPoint’?
121 | wxPaintDC dc(this);
| ^~~~~~~~~
| wxPoint
(cherry picked from commit 9ab5009235d212699f91e01d7f930f92849ed1e3)
* slic3r: Fix missing BOOST_LOG_TRIVIAL declaration
src/slic3r/GUI/wxMediaCtrl3.cpp:181:23: error: ‘info’ was not declared in this scope
181 | BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
| ^~~~
src/slic3r/GUI/wxMediaCtrl3.cpp:181:5: error: ‘BOOST_LOG_TRIVIAL’ was not declared in this scope
181 | BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
| ^~~~~~~~~~~~~~~~~
(cherry picked from commit c5c41e20ca2fc7f3b53a4c769961f73df6992008)
* FIX: wxMediaCtrl3 zero size crash
Change-Id: I16a3f7b3afe142bb957a1740b8e8c9820c92b349
Jira: STUDIO-8522
(cherry picked from commit 8cdaea1162ccbcc0bd03ecd99346f3b9cf52cf64)
* FIX: TabCtrl button margin
Change-Id: If8b05a4ef9efb8b57989ee1de6543631e5a3cf90
Jira: STUDIO-8265
(cherry picked from commit 1c5e65707109ad0582b6442cf8e515344f799c27)
* ENH: wxMediaCtrl3 display video frame at pts
Change-Id: I8847236d2307101e5f2befc6477cd20b3691841c
Jira: none
(cherry picked from commit 05328da4612c11d50f6fd90e872b97f5f8f46b1d)
* Fix: fix memory leak caused by ffmpeg decoding
Change-Id: I162ad4ea8d4601c1ffe17a65f292566c9dea6f0b
jira: no-jira
(cherry picked from commit eb20d03186c86b7398b97e3bae0a3c7a7b81c58c)
* ENH: update some missing codes
jira: no-jira
Change-Id: Icb2da53911430ac144b0fb601637a7ad31e7e8db
(cherry picked from commit 13b4213f8a24c76c16e49daf905fa29c0f646a5a)
* Fix build
* Update idle image
* Attempt to fix Windows CI build
* FIX: GTK video window resize ran in a free function without member access
wxMediaCtrl_OnSize referenced wxMediaCtrl2's private m_gtk_video_window,
which does not compile on Linux/GTK. Move the resizing into
wxMediaCtrl2::DoSetSize where the member is in scope.
* Install required tools for Linux
* Install required tools for macOS
* Add ffmpeg to flatpak
* Fix Linux build
* Try fix appimage build
* Fix Linux AppImage bundling of deps-built shared libraries
The AppImage dependency closure resolves each bundled ELF's DT_NEEDED
entries with plain ldd, which cannot resolve the deps-built FFmpeg stack
(libavcodec/libavutil/libswscale) once it is copied into the bundle:
those libs are not installed in any standard loader path and carry no
RUNPATH of their own, so ldd reports the siblings as missing and the
build aborts. Extend the loader path with the bundle directory plus the
source directories of already-bundled files (mirroring
scripts/check_appimage_libs.sh), and key the dedup set on the bundled
file path instead of the source path so dependencies resolved from the
bundle directory are not copied onto themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
* Attempt to fix Linux unit test
* Fix Linux unit tests loading deps-built FFmpeg libraries
The test executables that link libslic3r_gui (which links PkgConfig::LIBAV)
have a load-time dependency on the deps-built FFmpeg shared libraries. The CI
unit-test runner only receives the tests artifact, so those libraries were
unresolvable there (Ubuntu 24.04 ships libavcodec.so.60, not .61). Copy the
libraries next to each affected test executable and give it an $ORIGIN rpath,
mirroring the Windows branch that copies DLLs next to every test executable.
orcaslicer_copy_sos now places the copies in the per-config output directory
for multi-config generators, like orcaslicer_copy_dlls does.
Co-Authored-By: Claude <noreply@anthropic.com>
* Add design doc for macOS FFmpeg player
Co-Authored-By: Claude <noreply@anthropic.com>
* Add implementation plan for macOS FFmpeg player
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: use FFmpeg media player on macOS with static FFmpeg
* build: build static-only FFmpeg for macOS deps
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: remove old BambuPlayer-based media player from macOS
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add static FFmpeg NOTFOUND guard; drop dead wxMediaCtrl2.h include
* build: drop redundant --enable-shared in FFmpeg deps configure
The literal --enable-shared was always overridden by ${_link_cmd}
(--enable-static --disable-shared on Apple, --enable-shared elsewhere)
and FFmpeg configure processes these flags in order, last one wins.
Remove it and the stale comment documenting the workaround.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: remove dead wxMediaCtrl2 player
wxMediaCtrl2 was never instantiated on any platform (USE_WX_MEDIA_CTRL_2
is 0 everywhere); wxMediaCtrl3 replaced it. Delete wxMediaCtrl2.cpp/h,
drop them from the Win/Linux source list and the gettext list.txt, and
collapse the preprocessor-dead #if USE_WX_MEDIA_CTRL_2 gate in
MediaPlayCtrl.h.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: remove dead GStreamer bambusrc plugin and its build dep
gstbambusrc was the GStreamer source element for the old wxMediaCtrl2
Wayland player, its only consumer (deleted in the previous commit).
The new FFmpeg player handles bambu:/// URIs through the Bambu C API
instead. Drop the plugin and the gstreamer-1.0 / gstreamer-base-1.0
REQUIRED pkg-config dependencies that existed solely for it.
Co-Authored-By: Claude <noreply@anthropic.com>
* build: move FFmpeg media player sources to the common GUI list
wxMediaCtrl3 and AVVideoDecoder are platform-neutral C++ compiled on
all three platforms, so list them once in the common SLIC3R_GUI_SOURCES
instead of duplicating them in the APPLE and non-APPLE branches. The
else() branch is now empty and drops out entirely.
Co-Authored-By: Claude <noreply@anthropic.com>
* deps: disable FFmpeg VideoToolbox/AudioToolbox HW-accel on macOS
The static libavcodec.a/avutil.a compiled the auto-detected
videotoolbox/audiotoolbox objects, which reference VideoToolbox
framework symbols (_VTDecompressionSession*). The app link line
happened to satisfy them transitively, but the orca_stubgen module
link (CI-only) failed with undefined symbols. The player decodes in
software (swscale), so disable both HW-accel paths to keep the
static libs self-contained.
Co-Authored-By: Claude <noreply@anthropic.com>
* Copy the decoded frame instead of aliasing the decoder's buffer
wxImage with static_data set stores the pointer and never copies it, so the
frame handed to wxMediaCtrl3 aliased AVVideoDecoder::bits_. That buffer is
rewritten by the next sws_scale with the mutex released, reallocated by
bits_.resize() when the window grows, and freed outright when the decoder
leaves PlayThread's loop body at end of stream, all while the GUI thread may
be painting from it.
Windows is unaffected either way, since toWxBitmap already copies the bits
into GDI.
---------
Co-authored-by: chunmao.guo <chunmao.guo@bambulab.com>
Co-authored-by: BBL\chuan.he <chuan.he@bambulab.com>
Co-authored-by: MackBambu <yongfang.bian@bambulab.com>
Co-authored-by: Bastien Nocera <hadess@hadess.net>
Co-authored-by: chao.zhang <chao.zhang@bambulab.com>
Co-authored-by: lane.wei <lane.wei@bambulab.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
wxImage with static_data set stores the pointer and never copies it, so the
frame handed to wxMediaCtrl3 aliased AVVideoDecoder::bits_. That buffer is
rewritten by the next sws_scale with the mutex released, reallocated by
bits_.resize() when the window grows, and freed outright when the decoder
leaves PlayThread's loop body at end of stream, all while the GUI thread may
be painting from it.
Windows is unaffected either way, since toWxBitmap already copies the bits
into GDI.
It was already public and used elsewhere, so the new get_background_process()
and its duplicate forward declaration were redundant; Plater.hpp is now untouched.
exussum12 on PR #15238: "Esc hardly ever works". He was right, and the word
that matters is "hardly" — it works while you are mid-entity and stops working
the moment you finish one.
The branch asked whether a DRAW TOOL was armed, not whether a SKETCH SESSION
was open:
if (key == WXK_ESCAPE && m_viewport && m_viewport->is_sketching())
is_sketching() is DesignSketchTool::is_active(), true only between arming a
tool and finishing with it. Commit an entity and you are left at
ui_mode=Sketch with no tool armed, and from there Esc did nothing at all, no
matter how many times you pressed it — the Cancel button was the only way out.
Reproduced on the headless rig with SNAPORCA_KEYTRACE, which is what settled
it rather than reading: the key ARRIVES and focus is fine,
[KEYTRACE] key=27 ui_mode=1 is_sketching=0 in_text=0 inline_busy=0 focus=w
so this was never the focus problem it looks like from the outside.
Gate on the session instead. request_exit() is already layered — abort the
in-progress entity, else drop the tool to Select, else exit to Feature — so
widening the gate adds no new behaviour, it just lets the ladder be reached
from its own last rung. is_sketching() stays in the condition as an OR, so
nothing about the armed-tool path changes.
This is the same mistake snaporca-0ud fixed thirty lines above in the same
handler, where gating the sketch key MAP on is_sketching() made all 17 keys
read as dead. The comment there now has a sibling.
VERIFIED ON THE RIG, before and after, same sequence (Design > XY > Shift+S >
click canvas): before, two Escapes left the toolbar on SKETCH with zero pixels
changed; after, the toolbar reads FEATURES — one Esc drops the armed tool to
Select, the second exits the session.
Kernel suite green, 2568 assertions in 191 test cases. libslic3r_gui builds
clean on both forks. Parity 17 identical / 8 diverging as expected.
The recipe's 3MF entry moved from Metadata/SnapOrca_cad.bin to
Metadata/orca_cad.bin, so projects saved by earlier builds opened with an
empty Design tab. Both 3MF backends now read either name and write only the
new one; the recipe version advances to 6 to mark the move.
The tab is now gated on a new enable_cad_feature app-config key, off by
default, exposed in Preferences under General > Features. When off, the
page is never created, so the Design-only camera option is hidden too.
Takes effect on restart, like the other feature toggles.
Requested by SoftFever on PR #15238: ten of these had accumulated loose in
scripts/ next to ~20 unrelated upstream ones, with names that only meant
something to whoever wrote them. They now sit in scripts/CAD/, mirroring the
src/libslic3r/CAD/ and src/slic3r/GUI/CAD/ split, and the verb in the name is
the role: build- produces a binary, start- brings something up, run- runs a
suite, check- asserts one thing against a live app.
kernel-test.sh -> CAD/run-kernel-tests.sh
ladder-all.sh -> CAD/run-all-checks.sh
sketch-ladder.py -> CAD/check-sketch-engine.py
ladder-corpus.py -> CAD/check-sketch-engine-corpus.py
gui-ladder.py -> CAD/check-gui-sketching.py
offer-ladder.py -> CAD/check-gui-context-menu.py
mcp-sketch-smoke.py -> CAD/check-mcp-sketch.py
rig-build.sh -> CAD/build-gui.sh
docker-iter-build.sh -> CAD/build-gui-incremental.sh
gui-session.sh -> CAD/start-headless-gui.sh
"Ladder" was the worst of them: it named the shape of the test (rungs of
increasing difficulty) rather than what the test proves, so nothing in the
directory listing told you which one needed a GPU and which was pure kernel.
Every reference rewritten -- the docs, the cross-calls between the scripts,
Dockerfile.deps, and the container-side /OrcaSlicer/scripts paths. The three
shell scripts resolve REPO relative to themselves and now sit one level
deeper, so that walk went from /.. to /../.. . The copies these push into a
container's /tmp were renamed to match, or the container would have kept the
old names alive.
Two runtime paths deliberately NOT renamed. /tmp/orca-rig-build.lock is a
cross-fork contract -- both forks take the same lock so two concurrent builds
serialise instead of OOMing the box, and renaming it on one side silently
removes that guard. /tmp/gui-session.log is a runtime artefact, not a script.
Added scripts/CAD/README.md: what each script proves, what it needs, and the
two constraints that have each cost a session (never build inside the GUI
container; a window manager is required or synthetic keys are ignored).
On CI, which was the other half of the request: the kernel suite is already
there and always has been. The cases are registered in
tests/libslic3r/CMakeLists.txt under if (SLIC3R_CAD), which defaults ON and no
workflow turns off, so they build into libslic3r_tests and run under ctest on
every platform via unit_tests.yml -- like any other unit test, needing no new
job. They have simply never been seen to run, because the workflows on this PR
are still awaiting maintainer approval. run-kernel-tests.sh is the local loop
over the same cases, and it is the only script here CI could run: the other
six need an OpenGL canvas and synthetic input.
Verified: scripts/CAD/run-kernel-tests.sh from its new location, all tests
passed, 2562 assertions in 190 test cases.
Building the Design tab on first use (1750c52211) leaves m_design_panel null
until a human selects the tab. McpControl::handle_on_main refused every verb
while it was null, so start_mcp_control_if_enabled() opened the socket and
then answered "Design panel not ready" to everything — for the whole session
if nobody clicked. That is precisely the headless case the socket exists for:
the click-test rig drives this app over it with no window manager and no user.
MainFrame::ensure_design_panel() now builds the panel on demand and returns
it; the tab activation and the MCP dispatcher both go through it, so there is
one construction site rather than two. Safe to build wx controls there: that
handler is dispatched on the main thread by CallAfter, as its own comment
says.
The startup saving is untouched — a launch that never opens the tab and never
speaks MCP still builds nothing.
Verified: libslic3r_gui builds clean 745/745; fork parity green with snaporca,
which carries the same fix (McpControl.cpp is a byte-identical shared source).
* build: remove std::move that blocks copy elision
std::move wrapped around a temporary, or around a local being returned,
stops the compiler constructing it in place. Each edit is the fix clang
suggests, which is to delete the std::move call and keep its argument.
Three of the 39 sites save a move, the two return std::move(local) in
Print.cpp and TreeSupport.cpp:2749. The rest are equivalent either way
and match how the codebase already writes this elsewhere.
Clears 39 -Wpessimizing-move warnings.
* build: drop null checks on references and this
A reference cannot be bound to null and this cannot be null, so the
compiler folds these conditions to true and drops the guard. Seven are
if (&bitmap && bitmap.IsOk()), where IsOk() already does the work; two
test this directly. The guarded code runs either way, so removing the
dead operand changes nothing.
Clears 11 -Wundefined-bool-conversion warnings.
* fix(deps): build the dependencies from scratch with clang-cl
Six dependencies fail once the superbuild compiles them with clang-cl instead
of cl:
- OpenSSL never goes through CMake. Its VC-WIN64A makefile only works with cl,
and an unquoted clang-cl path with spaces produces no .obj files at all, so
the lib step dies with LNK1181. Pin the upstream toolchain.
- Boost.Container's bundled dlmalloc passes int* to the Interlocked API. cl
warns, clang rejects it.
- curl 7.75's configure probes rely on C laxness clang rejects. The results
flip and nonblock.c ends up in the AmigaOS IoctlSocket branch.
- OCCT installs RelWithDebInfo into bini/libi while find_package looks in lib.
It also prepends -Wl,-s to the shared linker flags for every Clang build,
which the MSVC-style linker gets as an argument it does not know. Both
patched hunks sit inside if (MSVC) in the OCCT sources.
- wxWidgets lands in lib/clang_x64_lib, so wxWidgetsConfig.cmake falls back to
the layout that exists instead of assuming vc_x64_lib. It tries the derived
path first, so a cl-built tree consumed by clang-cl keeps resolving the way
it does today. The patch step also resets the one file it touches, so it can
run again after an interrupted build or after the patch itself changed.
- wxInspector goes through FindwxWidgets, which only searches lib/vc*_lib
because _WX_TOOL is hardcoded to vc. It now gets the root and lib dir
derived the same way wxWidgetsConfig.cmake derives them.
Eigen is the seventh, and it breaks on the generator rather than the compiler.
Its test, lapack and blas/testing subdirectories all call
enable_language(Fortran), and they default to ON because the dependency
configures as its own top-level project. Whether that hurts depends on what
CMake finds: the Visual Studio generator supports no Fortran and finds nothing,
clang-cl sits next to the LLVM toolset's flang and works, while MSVC with Ninja
finds Strawberry Perl's MinGW gfortran, which this build already requires for
OpenSSL, and hands it the MSVC-style /machine:x64 that MinGW's ld reads as a
missing input file. The configure dies there and takes every dependency still
in flight with it. Only the headers are consumed here, so the three subprojects
are off.
* fix(deps): honor the superbuild's generator and compiler in sub-builds
orcaslicer_add_cmake_project pinned every dependency sub-build to the Visual
Studio generator whenever MSVC was true, which is also true for clang-cl. That
generator selects its compiler by toolset and ignores the CMAKE_C_COMPILER and
CMAKE_CXX_COMPILER this file already forwards, so the dependencies were built
with cl.exe no matter which generator or compiler the superbuild was given.
Key the three affected decisions on the generator instead: which generator the
sub-builds use, whether CMAKE_BUILD_TYPE is forwarded, and /m versus -j. A
Visual Studio superbuild is unchanged, so the default path and CI behave
exactly as they do today.
build_release_vs.bat now accepts -l to select clang-cl, alongside the existing
-x for Ninja, so the generator and the compiler can be chosen independently. On
the Visual Studio generator -l reaches the slicer only, through the ClangCL
toolset, because the dependency sub-builds have no toolset to inherit; a deps
build in that combination says so rather than quietly using MSVC.
* fix(deps): use upstream wxWidgets compiler layout fix
The compiler-prefix layout fix now comes from SoftFever/Orca-deps-wxWidgets#7, so remove the duplicated local patch and apply step.
* fix(deps): stop Assimp enabling ccache on the RC rule
ASSIMP_BUILD_USE_CCACHE defaults on and applies the launcher through the
global RULE_LAUNCH_COMPILE property, so it wraps the resource-compiler rule
as well. Under Ninja that rule goes through cmcldeps, which does not survive
being launched by ccache, and the build fails with clang-cl reporting /fo as
a missing file.
The superbuild already forwards CMAKE_<LANG>_COMPILER_LAUNCHER, which CMake
applies per language and so keeps clear of the RC rule.
---------
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: raistlin7447 <kris.austin@gmail.com>
Validate mixed-filament definitions during the published material pass: definitions whose components reference slots that do not exist or hold other mixed filaments, or that carry fewer than two components, are reported through the shared skipped_keys channel instead of shipping a mix the GUI integrity check would only flag later.
Fix the slot-limit exhaustion report being silently dropped: it wrote to published_config->skipped_keys, which the pass's final move-assignment from the local vector clobbers. All rejections now go through the local.
Remove the unreachable persist branch from add_detached_preset: no caller passes save_to_project=false, so the parameter is gone and the copy is always project-embedded.
Tests: cover the exhaustion path, the new definition validation, the identity-tier matching matrix (including substitute reporting), the structural-key denylist, whole-vector size-mismatch skips, relocation payload degradation, the "(Published 2)" uniquify chain, mixed blend colours staying out of shared preset configs, and duplicate-slot last-wins. Also fix the legacy-3mf scenario passing vacuously behind an if-guarded assertion. All existing published/3mf tests pass unchanged.
SLIC3R_CAD=OFF now builds without SolveSpace or OCCT's ModelingAlgorithms
module, leaving the dependency set identical to upstream's, and the Design
tab's mate connector preference no longer appears in builds without the tab.
When the deps prefix and the project disagree about the option, the configure
fails naming the cause, rather than failing at link time or at first launch
on Windows. The Windows packaging step stages exactly the toolkits libslic3r
links.
The Camera is Plater-owned and shared by every canvas, and Design sits
outside the panel switch that saves and restores it for Prepare,
Preview and Assemble — so orbiting in Design moved what the editor
tabs showed, and Design lost its own view on every switch. Trade the
live camera for a parked one on the way in and back on the way out, so
Design keeps its view the way Assemble already does.
New Project and Open Project went through Plater::priv::reset, which
drops ModelObjects but not the Model-level recipe, and never touched
the Design panel's document at all — so the previous design stayed
loaded and its next edit wrote itself into the new project.
A design that has not been committed to the plate has no ModelObjects,
so the project also read as clean: no autosave, and no unsaved-changes
prompt before the reset threw the design away.
DesignPanel's constructor creates several hundred controls and its own GL
canvas, which every launch paid for whether or not the user ever opened the
tab. The notebook page is now an empty placeholder and the panel is built
into it the first time the tab is selected, so m_design_panel stays null
until then.
This also lifts the ordering constraint that had pushed the plater's
background colour and Hide() below the panel construction; they go back
where they were.
build: clear 295 -Woverloaded-virtual warnings in GUI widgets
Turns three hidden base virtuals into real overrides, clearing 295 of
the 553 -Woverloaded-virtual warnings and taking a full clang-cl build
from 1,264 to 969. Part of #15374.
Search.hpp: SearchDialog::Popup and SearchObjectDialog::Popup took a
wxPoint that neither body ever read, hiding the virtual
wxPopupTransientWindow::Popup(wxWindow*). Both bodies clear the input,
call the base, set focus and refill the list, and SearchObjectDialog
also guards re-entry, so hiding meant none of that ran when the window
was popped through a base pointer. They now override and forward focus.
LabeledStaticBox::SetFont and ScrolledWindow::SetBackgroundColour hid
their base virtuals the same way, so the label metrics recompute and
the child colour propagation only ran for callers holding the concrete
type. Both now override.
Marking a member override makes clang flag every other unmarked
override in the same class, so seven sibling declarations needed the
keyword too. Left unmarked they were worth 481 warnings, which would
have made this a net loss.
MSWDismissUnfocusedPopup is declared only inside #ifdef __WXMSW__ in
wx/popupwin.h, so off Windows there is no base virtual to override and
the keyword would not compile. Both the declarations and the definitions
are guarded, which is how wxWidgets itself declares MSWWindowProc in
wx/nativewin.h and how this repo already handles it in BBLTopbar,
MainFrame, Button, ComboBox and TabCtrl.
ScrolledWindow's constructor left m_userPanel and m_scroll_win
uninitialised unless the style requested a vertical scrollbar, while
SetBackgroundColour dereferences both. No caller hits that today since
every instantiation passes wxVSCROLL, but the override widens who can
reach them, so they are now initialised alongside their siblings.
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
- Publish mixed-filament slots as whole units: serialize the filament_mixed_* definition into project_config on import, grow the receiver's parallel arrays in lockstep, and report unappliable definitions as skipped instead of dropping them silently
- Per-extruder printer selection: one inner tab per extruder, rows keyed by full "#N" ids; single-extruder receivers collapse variants onto their slot (first applied, rest skipped), multi-extruder receivers override element-wise
- New per-slot "Enable" toggle gating what gets published; enabling a mix auto-enables + Full Publishes its components
- Mixed page previews: fixed-size ratio bar, ternary triangle (3 components) and Material Ratio vs Model Height graph (gradients), always visible regardless of Enable
- Tab strip shows full swatch compositions with adjustable spacing; barycentric helpers shared via FilamentBitmapUtils
* Keep mixed-color filaments intact when the extruder count changes
The extruder-count spinner resized the filament arrays in bulk at the tail,
which is where mixed-color slots live, so a new filament landed behind the
mix and the sidebar skipped a slot number. It now adds and removes one slot
at a time through the same calls the sidebar's +/- buttons use, so a new
slot opens ahead of the mixed tail and a removal renumbers object filament
ids, painted facets, custom g-code and mixed components rather than
clamping them away.
Drops the vector overload of set_num_filaments(), which this leaves without
callers.
Update perimeter traversal to pass each extrusion's closed/open state into `apply_fuzzy_skin`. This lets fuzzy skin logic distinguish contours from closed loops when processing perimeters.
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
build: drop dead private fields, close malformed comments (227 warnings)
Clears 227 of the clang-cl warnings tracked in #15374, taking a full
Windows build from 1,491 to 1,264. Five of the six changes are in
headers, which are re-diagnosed in every translation unit that includes
them, so the count is large for a 14-line diff.
Tabbook.hpp: delete two private fields, unread since the 2022 import.
m_parent also shadowed wxWindowBase::m_parent.
GUI_Utils.hpp: the wxEVT_SYS_COLOUR_CHANGED lambda body is empty on
Windows, so its `this` capture is unused there. (void) this; leaves the
handler bound, which is what stops the event propagating.
DevFirmware.h: mark m_owner [[maybe_unused]]. The class is never
instantiated, and the file tracks BambuStudio, so this is the smallest
divergence.
Eight DeviceTab/ files, AMSItem.cpp and SelectMachine.cpp: block
comments malformed so that they read as a nested /*.
No behavior change. -Wcomment goes to zero, and only the three intended
categories move.
* Fix contour cleanup across coplanar triangles
Avoid generic collinear simplification after slicing. Skip only junctions created by shared edges between coplanar faces so contours stay stable without altering shallow geometry.
Fixes#15364
* Fix contour cleanup across coplanar triangles (code review fixes)
---------
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Call update_dynamic_filament_list() alongside update_mixed_filament_list() in two places: after editing a mixed filament slot and when the filament count doesn't change (e.g., adding a mixed/virtual slot). This ensures per-feature filament lists reflect the updated blended colour and type without requiring a full filament count change.
* Skip straight-run splits in corner smoothing
Teach `CornerSmoother` to treat vertices that only continue a straight segment as part of the same leg instead of rounding them as corners. The smoother now keeps a three-point window so it can emit a corner only once both adjoining legs are known, which avoids unnecessary corner processing while preserving real turns such as hairpins.
* Add regression test for split-leg smoothing
Adds a FillCornerSmoothing regression test covering polylines with an extra collinear vertex in a straight run. The test ensures corner smoothing treats split and unsplit geometry identically, preventing inconsistent rounding radii in triangular/grid infill paths.
# Description
This PR ports the color mixing feature from BambuStudio.
The port is based on the previous work by @ianalexis in #15231.
This PR completes the port and fixes various bugs.
Several improvements were also made during the porting process.
WIP
# 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.
-->
<!--
> 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)
* chore: mark every declaration that overrides a base virtual
clang-cl reports 42 member functions across 28 files that override a
base virtual without being marked `override`, inside classes that
already mark their other overrides. That is every occurrence of
-Winconsistent-missing-override in the tree, so the category drops to
zero and -Werror=inconsistent-missing-override becomes available as a
guard against it coming back.
Behaviour is unchanged. Each keyword goes only where clang had already
resolved the declaration to a base virtual, so it records what the
compiler already worked out and cannot affect overload resolution or
dispatch. If any of these signatures had not really overridden a base
method, the build would have failed rather than warned.
Where a declaration already carried `virtual` it is left alone and the
keyword appended, matching the surrounding declarations. Plain
`override` is used rather than the wxWidgets `wxOVERRIDE` macro, which
wx/defs.h defines as `override` beneath a comment marking it obsolete,
and which the rest of src/slic3r already avoids by 1742 occurrences to
113.
A full clang-cl build takes -Winconsistent-missing-override from 1,146
warning lines to 0. Those 42 declarations produce that many lines
because a header is re-diagnosed in every translation unit that
includes it. CalibrationWizardStartPage.hpp alone accounts for 336 of
them from 4 declarations.
* chore: drop unused lambda captures in GUI/Widgets
clang-cl reports 10 lambda captures in src/slic3r/GUI/Widgets that are
never read. Removing them changes nothing at runtime.
Every capture removed is `this` or a raw pointer. clang does not report
a capture whose type has a non-trivial destructor, since such a capture
can be held purely for its effect on an object's lifetime, so nothing
that owns or extends a lifetime is touched. The std::weak_ptr captured
beside the removed `this` in MultiNozzleSync.cpp stays.
This clears the category in GUI/Widgets only. A full clang-cl build
takes -Wunused-lambda-capture from 312 warning lines to 302, leaving
235 sites in other directories for a follow-up.
Port of snaporca 1bb9825db0. Four defects from an independent 20-agent audit,
each verified in the code first; two further findings from the same report were
verified OUT and are not in this commit.
remove_feature()/move_feature() remapped Extrude::sketch_ref and a Mate's two
connectors and nothing else, leaving seven of the nine index-bearing fields —
sweep_path_ref, loft_profile_refs[], pattern_curve_sketch, rib_sketch_ref, and
sketch_ref on Revolve, Sweep, Rib and the Surface* family — pointing at whatever
slid into the slot. Quiet by construction: the shifted index still names a real
feature, recompute() succeeds, the solid is built from the wrong profile. The
comment above the loop already required "EVERY field holding a feature index"; the
code under it handled two, because a type switch is only correct on the day it is
written. for_each_feature_ref() visits the FIELDS instead, so a feature type added
later is covered the moment it reuses one. plane_base and axis_plane_a/b are
excluded on purpose and documented at the helper — they encode an ordinal into the
datum-plane list, not an index into features[], and are filed separately. The
delete cascade got the same field-based treatment.
The regression test was run against the pre-fix code to prove it bites: all three
sections fail there, and move_feature returns TRUE while leaving sketch_ref == 1
where it must be 0 — success with the wrong answer, which is what makes this class
expensive.
apply_constraint, commit_entity_constraints and delete_constraint mutated the
recipe with no checkpoint() and no sync_recipe_to_model(), alone among seventeen
mutation sites in that file: Ctrl+Z reached past the constraint edit and discarded
unrelated work, and saving persisted the pre-constraint blob. A rejected constraint
now calls abandon_checkpoint() rather than leaving an undo step that does nothing.
MCP: params["generation"].get<uint64_t>() sat outside the try inside a bare
CallAfter lambda, so one malformed string terminated the process through the wx
event loop; it is type-checked now and the lambda lets nothing escape. The socket
bound with no mode of its own in a world-writable directory — umask around bind()
plus chmod, and it refuses to listen rather than listen wide. The reply write is no
longer a bare write(), which could SIGPIPE the app when a client hung up.
Kernel suite on this fork: 190 cases / 2562 assertions, green. GUI target compiles.
The full ladder gate ran on snaporca (ALL LADDERS HELD — gestures 98/98, offer
108/108, corpus and corpus-scale green) and fork-check parity holds at 17 identical
/ 8 diverging as expected, which is what makes that gate transferable here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
git apply inherits the caller's configuration, so under core.autocrlf=input it
rewrites the patched PCbuild/find_python.bat to LF. cmd.exe cannot resolve goto
labels in an LF batch file, so CPython's build fails with "The system cannot
find the batch label specified - begin_search" and then "Cannot locate
python.exe on PATH or as PYTHON variable".
git init already runs in the extracted source, so setting core.autocrlf on the
repository it creates is enough, without touching the shared PATCH_CMD.
User report 2026-08-23, and it is right: "you have renamed the feature extrusion,
not the body. i clicked rename on the body feature tree and the feature extrude
changed name. this means that you consider the extrusion = the body. this is very
far from truth as a body can contain several extrusions."
That is exactly what the previous commit did. It resolved a selected body to
CadBody::source_feature and renamed THAT feature, on the reasoning that a body has
no name of its own. The reasoning described an implementation detail — CadBody::
name is derived and restamped on every recompute — and mistook it for the user's
model. An Extrude, a Cut and a Fillet all land on the same body: the maker is one
operation in its history, and renaming it renames the wrong object.
A body now has a name of its own. CadBody::user_name, set only by a rename, is:
- carried across recompute() by body index, next to the per-body colour override
and under the same index contract the GUI already relies on for visibility and
Move — without which a name would survive exactly until the next feature;
- written into the recipe, because bodies are recomputed and never serialised, so
a name has nowhere else to live and would otherwise vanish on reopen;
- shown on the Bodies row ahead of the derived maker name, as "Body N — name",
with the number still leading because every status line, the interference
report and the mate errors identify a body that way.
The recipe block is APPENDED after the variables block rather than given a version
bump. A build that predates it reads features and variables, returns, and never
looks at the trailing bytes — so yesterday's projects open here and today's
projects still open there. A bump would have cost every project written today its
readability by the previous build, for one optional field.
Renaming a FEATURE is unchanged. The feature tree renames features; the Bodies
list renames bodies; neither reaches into the other.
WHAT THIS COST, and why it is written down. Getting here took two wrong turns
inside one fix, both mine:
1. UnselectAll() -> Unselect(). Both trees are wxTR_SINGLE, where UnselectAll()
— the MULTI-selection call — does nothing. That was the one-word reason the
rename had been vetoed everywhere (BEGIN_LABEL_EDIT refuses while
tree_body_selection() >= 0). Fixing it turned a pair of harmless no-ops into
a real loop: the two lists clear each other so "the target" is unambiguous,
so clicking a body row ran apply_body_row -> m_tree->Unselect() -> the feature
tree's SEL_CHANGED -> m_parts->Unselect(), which cleared the row just clicked.
The handler now clears the other list only when it actually holds a selection.
2. Trusting a screenshot taken after a polluted run. A leftover Rib card had
shifted the whole panel, so a click measured against it landed nowhere near
the row. Relaunch, then measure.
VERIFIED, on the rig and in the kernel:
body renamed ('Extrude', user_name=False) -> ('Bracket', user_name=True)
features untouched ['Sketch', 'Extrude'] before and after
survives a recompute add a Hole to the same body: features become
['Sketch', 'Extrude', 'Hole'], body stays 'Bracket'
survives the recipe serialize -> deserialize -> 'Bracket'
New kernel test "a body carries its own name, through recompute and the recipe"
pins all three properties; the suite is 189 cases / 2547 assertions.
Gate green: ALL LADDERS HELD — offer table matches the atlas, kernel suite,
engine rungs 1-8, 977-sheet corpus + the heaviest sheets, gesture ladder 98/98,
offer ladder 108/108.
User report 2026-08-23: "clicking on a body row in feature tree, I cannot find
rename on right click", then "still i cannot rename body1 in custom name".
TWO SEPARATE CAUSES, one in data and one in a single method call.
THE MISSING ROW WAS DATA. The `rename` verb's accepts list in the tool atlas was
["sk_loop"] alone, so the offer built for a selected BODY carried no Rename row.
Right-clicking a body row already opens the offer — that is the designed gesture,
bound on m_parts as wxEVT_TREE_ITEM_MENU — so the menu the user was looking at
was the right menu, and it was simply missing the verb. accepts is now
["sk_loop", "body_solid"], the generated table regenerated with it (the accept
mask moves 0x00004000 -> 0x00004080), and the offer trace confirms the row:
"[OFFER] row=7 Modify > rename".
THE RENAME ITSELF WAS BLOCKED BY UnselectAll(). Both trees are wxTR_SINGLE, and
wxTreeCtrl::UnselectAll() is the MULTI-selection call: on a single-selection tree
it leaves the row selected. So every path that tried to open the label editor
while a body row was selected hit the BEGIN_LABEL_EDIT guard — which vetoes while
tree_body_selection() >= 0, the rule that stops a body taking a name it cannot
keep across a recompute — and the editor never opened. Unselect() is the call
that works, and it fixes every route at once.
That took five attempts, four of them wrong, and the reason they were wrong is
worth more than the fix: each one addressed a plausible cause that the evidence
did not actually support — the popup's nested event loop, keyboard focus,
deferring with CallAfter, dispatching through a different verb. What settled it
was a DISCRIMINATOR rather than another fix: pressing F2 on a selected body row
takes the same handler with no menu and no nested loop. F2 failed identically,
which ruled out every menu-shaped theory in one measurement and left only the
state the veto reads.
A body still has no name of its own — it is recomputed from the recipe on every
change and CadBody::name is derived from the feature that builds it — so the verb
resolves the body to CadBody::source_feature and renames THAT, saying so on the
status line: "A body takes its name from the feature that makes it — renaming
'Extrude'". The Bodies row now reads "Body 1 — Extrude" so the rename is visible
where it was made; the positional "Body N" leads, because every status message,
the interference report and the mate errors identify bodies that way. Confirmed
as the wanted format by the user.
Also here, from the same report: the Bodies card keeps its own action row (Move,
Show / hide, Delete, Colour), and the competing context menu an earlier pass had
added to body rows is REMOVED — right-clicking a body belongs to the offer, and
two menus on one gesture is how the offer ended up being blamed for a veto.
Verified on the rig, both routes, with a body selected:
F2 on the body row -> ['Sketch', 'Extrude'] became ['Sketch', 'Base block']
the offer's rename verb -> {'applies': True, 'dispatched': True,
'selection_kind': 7, 'ok': True} and the same rename
Gate green: ALL LADDERS HELD — offer table matches the atlas, kernel 188 cases /
2532 assertions, engine rungs 1-8, 977-sheet corpus + the heaviest sheets,
gesture ladder 98/98, offer ladder 108/108.
RIG DISCIPLINE, repeated twice in one session and now written down: ladder-all.sh
does not relaunch the app, so hand-driving the rig immediately before a gate
leaves state its reset_document() cannot clear — both times the first rung drew
nothing and reported "sides []", which reads exactly like a broken rectangle
tool. Relaunch before gating.
* Update OrcaSlicer_tr.po
* Update OrcaSlicer_tr.po
Fixed inaccurate AI-generated text and updated missing translations.
* REmoive # AI Translated
* Update OrcaSlicer_tr.po
The following changes were made in this version:
- The term "Instance" was changed to "Eş kopya".
- The term "Jerk" was changed to "sarsıntı".
- Semantic discrepancies regarding certain words were corrected.
* Update OrcaSlicer_tr.po
The necessary arrangements have been made.
* Update OrcaSlicer_tr.po
* Update OrcaSlicer_tr.po
The necessary updates have been made.
* G-kodu to G-code
---------
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Adds a new "Length" row to the sequential marker position popup in GCodeViewer and updates row capacity accordingly.
For arc commands (G2/G3) that are split into multiple vertices, it now sums segment distances across vertices with the same gcode_id so the displayed value reflects the full move length instead of a single chord.
Introduced a shared `colinear_vertex_tolerance()` helper in `ExtrusionLine.hpp` and updated both simplify paths (`ExtrusionLine.cpp` and `WallToolPaths.cpp`) to use it instead of duplicated hardcoded `0.005` scaled thresholds. This keeps the near-colinear early-out tied to `SCALED_EPSILON` (rounding-noise scale) and avoids unintended curve decimation from larger tolerances, while documenting the geometric impact in code.
Two decisions from the user, 2026-08-23, after the first pass at making rename
reachable.
ONE SURFACE CARRIES THE VOCABULARY, and it is the row's own menu: Rename (F2),
Edit, then Move up, Move down, Show / hide, then Delete. Offered as a choice
between completing the menu or completing the header icons; the menu won because
the element you click answering with what applies to it is this fork's charter,
and because a menu grows without spending an icon nobody recognises. The header
icons stay exactly as they are — a quick bar for the common three — so nothing
that worked yesterday moved.
The menu is grouped rather than listed: what the row IS (name, contents), where
it SITS (order, visibility), and what removes it. Right-click SELECTS what it
points at before opening, so the menu can never act on a row other than the one
under the cursor.
MOVE BODY LEAVES THE FEATURE-TREE HEADER. It never belonged there: that header
sits over the FEATURE tree, and the button had to guess its subject from
whatever happened to be selected — a feature row got answered with "Select a
body to move it", an instruction about a different kind of object in a list that
does not contain one. A body now has its own action row on the Bodies card:
Move, Show / hide, Delete, Colour. The last three are deliberate COPIES of
feature-tree actions, not a reorganisation: a body row is a different subject,
and someone working in the Bodies list should not travel to another card to hide
or recolour what they have just selected. Each handler already resolves the body
row itself (on_toggle_visibility, on_delete_body, on_set_body_color), so the
card gives them a home and decides nothing.
The offer already carried Move for a selected body (verb `transform`, accepts
body_solid), so that half of the requirement was in place and is untouched.
One piece of the old button was function, not clutter: it also scaled imported
Text/SVG artwork, which IS a feature-row action. That moved to the row's own
menu as "Scale artwork", shown only on a row that has imported regions — so the
capability survives the split instead of disappearing with the button.
Verified on the rig, by the gestures themselves: right-click a row -> the six
items in their three groups; choosing Move down turned ['Sketch1', 'Sketch2']
into ['Sketch2', 'Sketch1']; and with a body present the Bodies card shows its
four actions while the feature header no longer shows Move.
Gate green: ALL LADDERS HELD — offer table OK, kernel 188 cases / 2532
assertions, engine rungs 1-8, 977-sheet corpus + the heaviest sheets, gesture
ladder 98/98, offer ladder 108/108.
RIG DISCIPLINE, learned the expensive way in this session: ladder-all.sh does
NOT relaunch the app, so hand-driving the rig immediately before it leaves state
the ladder's reset_document() does not clear — here the first rung drew nothing
at all and reported "sides []", which reads exactly like a regression in the
rectangle tool. Relaunch the app before a gate, and re-run before believing a
failure that appears in rung one.
User report 2026-08-23: "on feature tree, i cannot rename sketch name".
The rename was not broken. Verified on the rig before changing anything: select
the row, press F2, type, Enter — Sketch1 becomes Base, the name reaches
m_doc.features[idx].name and sync_recipe_to_model() persists it. The offer's
btn:rename verb does the same. What was missing was any way to find that out.
Everything a person would try did something else:
- the pencil in the section header is EDIT (on_edit_feature)
- a double-click fires wxEVT_TREE_ITEM_ACTIVATED, which is also Edit
- none of the seven header icons renames
- right-clicking a row did nothing at all
and the comment above the label-edit handlers claimed a "slow double-click"
renames, which does not survive wxGTK: the activation wins and the sketch opens
for editing instead. So the only route was an undocumented function key, and the
report is exactly right from where the user stands.
The row now answers the gesture people actually use on a named row: right-click
gives Rename (F2) / Edit / Delete, with Rename opening the in-place editor on
that row. Right-click SELECTS what it points at first, so the menu can never act
on a different row than the one under the cursor.
And selecting a row now says what the row can do: "Sketch1 selected — F2 or
right-click renames it, double-click edits it". Cheaper than a tooltip nobody
hovers, and it uses the status line that already exists for exactly this.
A note on the surface, since it is a design call: the CANVAS right-click is the
offer, this fork's single adaptive menu, and this is not that. A tree row is a
different surface, and its menu is three items about the row. Routing tree rows
through the offer would mean teaching the offer a selection kind that is not
geometry, which is a larger change and not what this report needed.
Verified on the rig by the gesture it names: right-click the row, choose Rename,
type, Enter -> ['Sketch1'] becomes ['Base profile'] in describe_scene.
Gate green: ALL LADDERS HELD — offer table OK, kernel 188 cases / 2532
assertions, engine rungs 1-8, 977-sheet corpus + the heaviest sheets, gesture
ladder 98/98, offer ladder 108/108.
The sidebar Mixed Filament list, the extruder icons, the color painting
gizmo and the canvas filament bar now show the same bottom-to-top fade the
Edit Mixed Filament preview shows, custom gradient curves included, instead
of a horizontal fade between the two component colours. Ordinary and vendor
multi-colour filaments are drawn exactly as before.
Mixed-colour slots are virtual and never flushed. Guard auto_calc_flushing_volumes_internal against them as BambuStudio does, and make the flushing dialog's default matrix and the sidebar 'modified' comparison physical-only so the untouched mixed rows no longer count as a user edit and the Re-calculate result matches the physical-only table.
Two user reports from the same session on the deployed build, 2026-08-23.
FIRST: "if I select a shape (es a circle draw in construction lines) and then I
try to toggle contruction to obtain a full line, does not work". Reproduced: Q
converts the selection and so does the offer's Reference > Construction row —
both run m_keys_sketch['Q'] — but the CHECKBOX, the one control actually
labelled Construction, only ever called set_sketch_construction(), which arms
the mode for the NEXT entity. So the obvious control was the one route that
could not convert existing geometry, and it failed silently while also flipping
the draw mode behind the user's back. It now carries Q's meaning.
Scoped to Select mode, and that scoping is not cosmetic: drawing AUTO-SELECTS
what was just drawn (draw-then-edit), so with a draw tool armed "there is a
selection" does not mean the user picked anything — it means they finished a
line. The first version converted there and turned the box into a trap: arm
construction, draw the axis, click the box to go back to real geometry, and
instead of disarming the mode it converted the axis just drawn. The gesture
ladder's C4 rung does exactly that and reported three construction entities
where it wanted one. In Select mode the intent is unambiguous.
SECOND, and this one destroyed work: "after creation of a circle, a round angled
rectangle and a slot, and mirror of those shapes on a vertical line inside a
outer rectangle, preview is ok but application creates errors: the circle is
mirrored, but rectangle and slot are redrawn as pieces of circles screwing both
the original shapes and the copies." A screenshot came with it, and it showed
more than the words did: the ORIGINALS were wrecked too — the rounded rectangle
was drawn as a four-lobed cloud, each corner fillet having gone the long way
round, and the slot had ballooned into two near-full circles.
Measured on the rig, a slot mirrored about a vertical line:
rails 62.873 / 62.873 -> 2.082 / 62.913
caps r=21.554 sweep=-180.00 -> r=32.214 sweep=-237.66
and all four sources moved, the axis line with them
Cause: confirm_op's Mirror branch bound an Arc copy to its source with a
Symmetric constraint on the CENTRE ALONE. An arc has five degrees of freedom;
pinning two of them leaves the endpoints and the sweep free while the shape's
own coincidences still pull on them, and the solver answers with a different,
internally consistent sketch — which is what a reflex cap and a 2 mm rail are.
A circle came through the same code untouched because a circle HAS no endpoints
to leave free, which is exactly why the failure reads as "circles fine, rounded
rectangles and slots destroyed".
Three parts, and each one is here because the measurement caught the previous
one being half a fix:
1. Arcs are bound by BOTH ENDPOINTS. Endpoints before centre in the ladder:
{p0, p1} is four equations against five DoF and pins the sweep, while
{centre, p0, p1} is six and is refused — the refusal is what silently
degraded the batch to a set that left the sweep free.
2. Every copy is reflected from the PRE-BATCH source, so a batch that disturbs
the sketch cannot hand the next copy already-moved geometry.
3. THE APPLIED RESULT IS THE PREVIEW — checked on the sources AND the copies,
and on violation the whole constraint web is dropped and both halves are
restored to the reflection the preview drew. try_add_constraints rolls back
only when a solve FAILS, and every failure here came from a solve that
succeeded at something else. Guarding only the sources fixed the slot and
left the rounded rectangle's copies at a 13.8 mm rail and a 308 degree cap:
the original was safe and the copy was still wrong, which is half a fix.
The parametric link is kept whenever it provably holds the geometry, and dropped
when it does not. A wrong shape is worse than an unlinked one.
WHY NOTHING CAUGHT THIS: the gesture ladder's mirror rung reflects three
straight LINES. It sat green through the whole defect. C4b now mirrors a slot,
so the reflection has arcs in it, and grades the property the user actually
stated: the copy is the source reflected, the source does not move, and no cap
comes back reflex.
VERIFIED against the user's own scene, rebuilt gesture by gesture on the rig —
outer rectangle, circle, rounded rectangle and slot, a vertical CONSTRUCTION
line as the axis, all 17 entities mirrored in one gesture:
ok the mirror axis is a construction line
ok picked the axis and all 17 entities
ok originals unchanged (moved: [])
ok every copy is the exact reflection (worst 0.000000000)
ok no source arc turned reflex — the 'cloud' failure
ok no copied arc turned reflex (6 arcs checked)
One grader correction worth recording, because it cost a round and would cost
the next one too: a reflection REVERSES ORIENTATION, so a copy legitimately
stores p0/p1 the other way round. Comparing p0 to p0 grades the storage order,
not the geometry, and reported a perfect mirror as an 8.98 mm error. Endpoints
are compared as an unordered pair.
User report, 2026-08-23, after using the freshly deployed build: "selection and
removal of existing elements of the 2d sketch is not intuitive, and the bottom ui
text does not illustrate what the user has to do to properly use the selected
tools. Mirror, for example, does not indicate: first select mirror line then
entities to be selected, and there is no UI indication of what is being selected.
normally, costruction lines are dotted." Four defects, all of them in the 2D
vocabulary this fork's charter puts at the centre, and all four fixed here.
snaporca-1c0c (P1) — the prompt was written ONCE, when the tool was armed.
DesignPanel's select_tool lambda set a sentence and nothing ever revised it, so
every step after the first was unguided: Mirror said "pick axis, then entities"
and then never said which of the two you were on; Escape silently downgraded an
armed tool to Select (request_exit is layered: anchors, then tool, then session)
while the line still named the tool you had left; and nothing ever mentioned that
Del removes a selection. The fix moves the line off the arm event and onto the
tool's LIVE state. DesignSketchTool::emit_step_hint() reports (mode, step, picks)
whenever that triple moves, from render() — the one place every state change in
this tool passes through. Putting it there instead of in the thirty-odd branches
of on_mouse is the whole point: a per-call-site notification is a thing the next
tool forgets to add, and it costs three int comparisons a frame. DesignPanel owns
the words, in ONE table (sketch_step_prompt), whose step numbers are the same ones
render() previews and on_mouse consumes, so the description cannot drift from the
code that reads the clicks. Every tool now names the gesture that ENDS it, because
none of them was discoverable: an empty click applies an edit-op or a transform,
right-click cancels it, Esc goes back to Select.
snaporca-vd6v (P2) — Mirror mirrors its axis pick and its target picks into
m_selection, so both painted white and the picture could not answer "what did I
select as what". The edit-op's first pick — Mirror's axis, Fillet/Chamfer's first
line — now paints violet. Violet and not cyan: cyan means SELECTED in this canvas
and nothing else may wear it, a rule this file already carries in writing.
snaporca-imlq (P2) — construction geometry drew as a solid grey line. Every CAD
dashes it, and grey alone does not read as "reference" against the
under-constrained orange. dash_polyline() chops the polyline before it reaches
draw_quad_strip, with the dash and gap in world units scaled by units-per-pixel,
so a dash keeps its size on screen instead of becoming a solid line when you zoom
out and three dashes when you zoom in.
snaporca-oql1 (P2) — Backspace now deletes as Del does. On every laptop this runs
on, Del is a chord and Backspace is what a hand reaches for. The Select-mode
prompt states the rest (Shift-click adds, double-click takes the loop, Del
removes), and the first step of every armed tool names the Esc route back to
Select, which was the invisible half of "selection is not intuitive".
Also, on the same report: the sketch stroke half-width goes 0.6 -> 0.3 mm. At 1.2
mm wide the orange line swallowed a short segment and hid which of two near
parallel lines the cursor was on. One constant, because all twenty call sites of
draw_quad_strip are sketch strokes.
Retired on the way: the on_sketch_selection_changed status writer. It said "N
selected — Delete removes them" while an edit-op mirrored its picks into the
selection, i.e. in the middle of a Mirror gesture, where Delete does nothing of
the sort. on_sketch_step says the true thing for Select and says nothing false
anywhere else. And the live length/angle readout is now APPENDED to the step
guidance rather than replacing it: it fires on every mouse move, so it used to
erase the instruction for the step in progress one move after the click that
started it.
Two false trails, recorded so the next session does not walk them again:
- DesignPanel.hpp deliberately does not include DesignSketchTool.hpp, so the
panel's handler takes the mode as an int and the .cpp casts it back. The
first attempt put Mode in the header signature and the build said only
"expected ',' or '...' before 'mode'".
- The offer ladder failed six properties against a perfectly good binary
because I had relaunched the rig myself without SNAPORCA_KEYTRACE=1, and its
[OFFER] trace lines ARE its instrument. A ladder with no instrument reports
"None", which reads exactly like a regression in the offer. ladder-all.sh
launches it correctly; a hand relaunch must too.
VERIFIED, not merely compiled. Driven on the headless rig with synthetic mouse
and keyboard, and photographed at each step: "Mirror — first click the LINE to
mirror about (a construction line works) · Esc goes back to Select" ->
"Mirror — axis set · now click the entities to mirror · right-click cancels"
-> "Mirror — axis set · 1 to mirror · click another to add or remove it ·
click empty space to apply", with the axis violet, its target white, and the
construction lines dashed while real geometry stays solid.
Full gate green afterwards (scripts/ladder-all.sh, ALL LADDERS HELD):
offer table vs the atlas OK
kernel suite 188 test cases, 2532 assertions
engine ladder rungs 1-8, ALL RUNGS HELD
corpus rung 977-sheet drawing corpus, every 20th -> 49
sampled, 39 gradeable, 39 clean
corpus scale rung the 6 heaviest sheets, all clean
gesture ladder 93/93 properties, real mouse and keyboard
offer ladder 108/108 properties, through the right-click
menu and the verbs behind it
That harness is the reason a UX change of this size can be made in one pass and
believed: 93 + 108 properties are driven the way a person drives the app, and the
977-sheet corpus keeps the engine underneath them honest against real drawings
rather than against my own arithmetic.
It passed alone and failed inside scripts/ladder-all.sh, twice, on the same property: "right-click
with two picked -> None". The diagnostic it now prints said what a screenshot could not — editing=True
with an empty selection after two clicks that should have picked two entities. Three rig facts, all
about the DRIVER, none of them a product defect:
A LEFT-CLICK ON A LINE'S MIDDLE OPENS ITS LENGTH FIELD. The Select branch tests m_live_quotes before
it picks, with a ~24 px label tolerance, and a line's Length quote sits at its midpoint — so the
click that was meant to select it promoted a dimension and froze the canvas instead. Everything
after it landed on nothing. It bit only when the previous step had left that line selected, because
live quotes are drawn for the SELECTION: hence passing alone and failing in the gate. Lines are now
picked at 0.3 along, clear of the label.
A FIELD THAT HAS NOT OPENED YET READS LIKE ONE THAT NEVER WILL. The queue opens each field from a
CallAfter, so a driver that looks once, sees nothing and moves on gets frozen by the field that
arrives a moment later. keep_as_drawn() now waits for QUIET — two consecutive clear readings — and
draw_line_at drains stragglers before handing back.
A KEY CANNOT CANCEL A SESSION WHOSE CANVAS IS FROZEN. gui-ladder's enter_sketch dismisses the old
sketch with Escapes, which an open field swallows, so the session survived and the four calibration
probes landed in it on top of what was already there: "calibration expected 4 points, got 7". Every
rung now enters through fresh_sketch(), which cancels through the socket first — that cannot be
swallowed.
Measured after the fix, in the sequence that failed: gesture ladder 93/93 then offer ladder 108/108,
back to back on the same app.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
snaporca-ziam said gen_offer_table.py would silently delete the model-mode "Constrain sketch"
row, because that row lived in the generated header and not in tool_atlas.json. Running it found
more than that: FOUR rows existed only in the header — constrain, rename, and the three typed-
value rows sk_length / sk_radius / sk_angdist — and sk_delete's action had drifted, pointing the
sketch row at btn:delete, the FEATURE delete.
All five are now in the atlas, so the header regenerates byte-identically from it. Verbs may carry
a `note`, emitted as a C++ comment above the row: a rationale written into a generated file is
deleted by the next regeneration, which is how this started.
snaporca-z8rs (P1), found by making that true: after the atlas held all 92 verbs, the regenerated
header differed from the checked-in one by EXACTLY ONE LINE — kOfferVerbCount, 91 against 92.
Every consumer loops i < kOfferVerbCount, so the last row of the table was invisible: never listed
by show_offer_menu, never findable by mcp_run_verb. The verb that fell off the end is sk_angdist,
"Angle / distance…" — the typed-value row for a two-entity selection. On the one selection where
you would ask for the angle between two lines, the row that types it was not in the menu.
It survived because nothing compared the Sk2Ent menu against the table: sk_angdist accepts Sk2Ent
and nothing else, so an off-by-one that dropped the LAST verb was invisible from every other
selection. The vocabulary rung now covers Sk2Ent too, and picking the pair taught it one more
rig fact — shift-clicking a circle at its +X point grabs the RADIUS GRIP, which replaces the
selection with that one entity, so the pair silently collapsed to one and the offer answered
SkLine. Correctly, for the selection that actually existed.
gen_offer_table.py --check proves header == atlas and changes nothing; it is now the first step of
scripts/ladder-all.sh, and the only one that needs no rig.
Offer ladder 107/107, gesture ladder 93/93, both on the rig.
snaporca-ziam snaporca-z8rs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
The 2D vocabulary is 46 verbs: 22 have a shortcut and the gesture ladder drives them, 24 have
none and nothing had ever exercised those. They are reachable only from the right-click offer, so
a key-driven ladder could not have touched them whatever it did. Four new rungs drive all 24, and
the coverage claim itself is now arithmetic against DesignOffer.hpp (rung O8) rather than a
sentence in a comment that rots when a verb is added.
The assertions are CONSTRUCTION invariants wherever a click cannot be exact — a regular polygon's
sides are equal to 1e-9 and its vertices lie on one circle; a tangent arc's radius at the shared
endpoint is perpendicular to the line to 1e-9 (measured cos 5.97e-17); the three clicks of a
3-point circle all lie on it; a circumscribed pentagon's circumradius is the inscribed one's over
cos(pi/5), 1.236067977 against 1.236067977. Where a value field opens, the typed value is graded
exactly: a moved line travels +25.000000000 in X and 0 in Y, a rotation turns 30.000000000 deg
and leaves the length alone, a scale multiplies it by exactly 3, a linear array's pitch is
[20.0, 20.0, 20.0] and a polar one's spokes are 60 deg apart all the way round.
Three defects found doing it, all fixed here:
snaporca-ua9g (P1) — delete_selected left three things behind. The AUTO-EDIT QUEUE, so a queued
field opened on a deleted entity and its commit went nowhere: draw a rounded rectangle, delete
everything, draw a 2-point circle, type 30 — the field opens, the digits are accepted, and the
radius stays 32.992020763. reset_autoedit() exists for exactly this and its own comment says so;
it was simply never called from here. The FEATURE GROUPS, whose [begin,end) ranges all shift on a
delete, so feature_of() answered with a group the user never drew — survivors are now remapped
and any group that lost a member is dropped, the rule the placed quotes already followed. And the
SOLVER STATE: no re-solve, so sketch_describe reported dof=16 for a document holding one circle.
snaporca-ekt9 (P2) — the read-back could not see three of its seven entity types. Ellipse,
EllipseArc and BSpline serialised as a bare type name: no centre, no semi-axes, no rotation, no
sweep, no poles. gui-ladder's ellipse rung had to grade the faceted area of the loop at 2e-2 —
that tolerance IS the faceting error — and its spline rung could only count entities. Now they
carry their parameters, and the ellipse arc's ends are asserted to satisfy (x/a)^2+(y/b)^2 = 1 to
1e-9.
Also read-only, and the reason the other two were found at all: sketch_describe now reports the
armed TOOL, the count of PENDING anchors, and whether a value field is EDITING. A menu walk that
lands one row off arms a neighbouring tool and then draws something plausible — the first run of
the authoring rung drew a circle of area 45238.93 and graded it as a rectangle. Every menu pick
now asserts which tool it armed, and the polyline rung (a per-segment Length field freezes the
canvas after every click) could only be written once the driver could ask whether a field was open.
Offer ladder 102/102 -> 105/105 with coverage. Gesture ladder 93/93 and the kernel suite
188 cases / 2532 assertions, both unchanged.
snaporca-ua9g snaporca-ekt9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
The gesture ladder proved the TARGET — a complex closed profile, exact in vertices, lengths, arcs
and symmetry, voids correctly attributed. It proved it by arming every tool with a letter key,
which leaves the goal's own MECHANISM untested: the design logic pivots on right-click, and the
verbs offered are supposed to adapt to the element under the cursor. 47 of 86 Design-tab verbs
have a GUI action and no shortcut, so a key-driven ladder cannot reach more than half of them.
scripts/offer-ladder.py drives the menu. It asserts nothing from pixels: show_offer_menu emits an
[OFFER] trace from the same loop that builds the rows (behind the existing SNAPORCA_KEYTRACE), so
what the ladder reads cannot drift from what the user is shown, and the expected row set is
predicted by parsing DesignOffer.hpp rather than transcribed by hand. 25 properties, four rungs:
what each element type offers, that the menu equals the table for four selections AND that the
four differ, a 120 x 80 profile authored entirely through the menu, and a tool with no keyboard
route at all driven from the only door it has.
Two real defects, both found by it, both fixed here:
snaporca-ghcz (P1) — right-click was a black hole while any draw tool was armed. Every draw case
ended with `if (evt.RightDown()) { m_points.clear(); return true; }` and returned true even with
nothing to abandon; on_mouse records that in m_right_consumed and DesignCanvas suppresses the
offer whenever it is set. Measured: with Line armed, two right-clicks in a row produced no menu
and no tool change; only Escape freed it. Same rule snaporca-xmh6 wrote for the selection —
clearing nothing is not a gesture terminator. One shared right_abandon() now consumes the click
only when an anchor was really down; 16 sites, plus Polyline/BSpline (which end a chain, correct
only when there IS one) and Point (which has no anchor at all).
snaporca-lnri (P2) — right-clicking a sketch point offered the empty vocabulary. select_at_screen
tests hit_test_point first and records the hit in m_point_sel, but the offer counts m_selection
only, so a Point entity could never reach the entity branch and SkPoint was unreachable by
construction. A Point IS its own handle, so it is selected as an entity; other entities keep the
handle pick, since a line's endpoint is a drag target, not a vocabulary.
Offer ladder 25/25, gesture ladder 93/93 (no regression), both on the rig. The offer ladder joins
scripts/ladder-all.sh as the fifth rung.
snaporca-ghcz snaporca-lnri
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Draw-then-edit is armed from a jump in the entity count: render() sees n > m_autoedit_seen,
selects the last entity and schedules open_primary_autoedit. A scripted add made while a
creation tool was armed looked exactly like a drawn gesture, so it opened that tool's value
field — and an open field freezes the canvas (on_mouse_impl returns early on m_awaiting_length)
and swallows every letter (in_text includes inline_busy()). Measured on the rig: after
sketch_add, 'p' + click added nothing (4 entities before, 4 after); one Escape and the identical
sequence gave 5. It also explains the selection = [last index] that sketch_describe reported
although action_sketch_add never selects anything — the render pass wrote it.
Escape worked because it sequences two set_tool calls: the pending CallAfter fires between them,
so the second one commits the field it finds open. Arming a tool directly is one call, and the
CallAfter fires after it.
Fix: resync m_autoedit_seen at the end of add_entities_scripted, so a scripted add is not read as
something the user just drew. An already-open field is left alone. Covers sketch_add,
sketch_mirror and sketch_offset — the three callers.
The scale rung's Escape workaround is deleted, which is the issue's acceptance criterion; it is
now the regression test. Gesture ladder 93/93 on the rig.
snaporca-j7gc
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Over the whole 977-sheet corpus: 767 graded, 767 fully clean, 0 failures. The 210 skips are sheets whose part outline is not a closed stroked path at all (largest loop 5 to 132 mm2, measured), and one of them — MPD133 — is password-protected, which was being reported as an engine ERROR. Both now say what they are.
snaporca-j6sr
Two commits carried across (snaporca 579a9a9162, f68613cfc5).
Past about 480 entities a sketch had NO constraints at all and said nothing: libslvs
declares MAX_UNKNOWNS = 1024 and is handed every entity in the sketch at two params
per point, so the whole system came back TOO_MANY_UNKNOWNS and try_add_constraints
rolled the entire inferred batch back. From there no dimension could ever be applied.
Constraints only couple entities that share a point, so the solver now falls back —
only on TOO_MANY_UNKNOWNS — to solving connected components separately and committing
all-or-nothing. The auto-constraint pass batches its Horizontal/Vertical constraints
instead of one solve each, which is what kept the bulk path fast once solves started
succeeding: a 1204-entity load went 1585 ms -> 562 ms.
Plus the scale rungs (a thousand-entity plate drawn on by hand; the heaviest real
drawings graded and timed), the --step 1 fix that used to select nothing while
reporting a clean run, and scripts/ladder-all.sh as the one-command gate.
Parity 17 identical / 8 diverging as expected. Kernel suite here: 188 cases /
2532 assertions, including "a sketch past the solver's unknown limit still solves".
snaporca-yww4, snaporca-x6v7, snaporca-j6sr
and a ladder that draws with the mouse
Three commits carried across (snaporca 4ffd60eacb, 421055c2ec, b71216ce0b):
1. A design made only of sketches must survive being saved. CadDocument::recompute
returned false with "no solid-producing features" for a document that has no
solid, and two callers read that as "unusable": the GUI syncs the 3MF recipe
only after a successful recompute, so a sketch-only design was saved with no
recipe at all, and deserialize_recipe ends with `return recompute()`, so even a
project that carried one was refused on load. Having nothing to build is now a
success; a feature that MEANT to build a solid and produced none still fails.
DesignPanel::refresh_tree syncs the recipe too, for the paths that call
m_doc.recompute() directly.
2. Scripted geometry arrives exact. The Horizontal/Vertical inference window and
the endpoint weld window both close to zero for add_entities_scripted; void
attribution probes from a point strictly inside each loop instead of from its
first vertex. Corpus rung 39 graded / 39 fully clean, was 35 with 6 failures.
3. scripts/gui-ladder.py — 17 rungs, 84 properties, all driven by synthetic clicks
and typed values rather than through the socket.
Parity 17 identical / 8 diverging as expected. Kernel suite here: 188 cases /
2532 assertions.
snaporca-mtav, snaporca-8xg1, snaporca-5hvl, snaporca-730j
Carries snaporca 9f0a6656bc.
Mouse3DController::apply DRAINS the input queue and every BOUND canvas idles and calls it, but a
hidden canvas's render() early-returns on _is_shown_on_screen() — so it swallows motion, applies
it to the shared plater camera, and draws nothing. The next visible frame jumps by more than one
state change. The plater keeps exactly one of its three views bound; the Design canvas binds once
at construction and never unbinds, so two canvases drain the same queue.
Guarded at the apply site so only the canvas actually on screen takes motion off the queue,
whatever happens to be bound, and without touching the plater's view-switching state machine.
Reported by exussum12 on PR #15238 as lag and jerkiness in the Design tab against "really smooth
on the other tab"; he guessed the mechanism correctly. NOT verified with a device — there is no
SpaceMouse here and the rig has no HID, so this is a mechanism traced in source and matched to a
user's description, not a measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 0231bd5b68. Parity holds: 17 files identical, 8 diverging as expected.
A reflection reverses orientation, so mirror_entities now hands the reflected half back reversed
in ORDER and flipped per ENTITY — an arc swapping its angles as well as its ends, a spline
reversing its control points. Appending it to the source then yields one walkable chain instead
of two halves meeting head-to-head, and a mirrored CCW loop stays CCW.
This is the producer half of the confusion that cost three defects; the consumers (offset, and
the exact loop area) keep their defensive handling, because that is what makes them correct for
hand-built and imported sketches rather than only for geometry this function produced.
Contract change, carried with the reason: a mirrored line's p0 is the reflection of the SOURCE's
p1, and a mirrored CCW arc keeps a POSITIVE sweep — the reflection negates it, walking it the
other way negates it again. Both [SketchEdit] cases updated, and a new [SketchProfile] case
"a mirrored half continues the original chain" pins the property directly.
Kernel here: all tests passed, 2687 assertions in 232 test cases. GUI target builds and links.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 572f794c84, d0f9a0052a, 9f7e4e3627 and 3974f8a170. Parity holds: 17 files
identical, 8 diverging by their expected counts.
EXACT AREA. A loop's area is now integrated entity by entity in traversal order — Green's
theorem — instead of being shoelaced over the render polyline, which faceted every arc into 24
chords and lost 2.02 mm2 on a 3706.86 mm2 stadium. 0.054%, invisible on screen, and wrong in a
number reported as "the area".
OFFSET FOLLOWS THE TRAVERSAL. Offsetting a mirrored profile put one half on the wrong side and
split the loop in two, because the chainer only followed p1->p0 links and each entity's offset
side was taken from its stored direction. Chains are now orientation-aware, seeded at a free end,
offset by `reversed ? -d : d`, and normalised head-to-tail on the way out — so offset is correct
for any input ordering and its own output cannot reintroduce the problem.
Both are the same underlying lesson, which has now cost three separate defects: an entity's
STORED direction is not its direction of TRAVEL around the loop.
THE LADDER. scripts/sketch-ladder.py is a graded suite of 2D sketches judged the way a person
judges them — VERTEX, LENGTH, ARC, TANGENT, SYMMETRY, CLOSED — with area only as a cross-check,
because area is derived and nobody can confirm it by eye. Eight rungs from a rectangle up to
MPD5 from the StudyCadCam corpus, a dia 27 x 95 pin reproduced as its revolve half-profile with
the R5 fillet tangency solved exactly. Entirely 2D: no extrude or any solid feature.
Kernel here: all tests passed, 2681 assertions in 231 test cases, including the new
"profile: a mirrored half offsets as one loop, not two". GUI target builds and links.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 95e59289f9, faec177d42 and 20df726ecb. Parity re-verified: 17 files identical,
8 diverging by their expected counts — DesignCanvas.cpp back to 16 and DesignPanel.cpp back to
32, which is the proof each hunk landed on the right side of the FeatFlyout and TAB_ID_PREPARE
divergences rather than on top of them.
All three answer exussum12's review on OrcaSlicer PR #15238.
ENTER/ESC IN THE VALUE FIELD. The field is a borderless always-on-top frame, and whether it may
hold keyboard focus is the platform's decision — a borderless NSWindow can never be key, and
mutter refuses a re-mapped window. When focus is denied the keys reach the panel instead and the
queued-dimension chain (a line queues Length then Angle) cannot be walked. The CHAR_HOOK now
forwards Enter/Numpad-Enter/Tab/Esc to the field when it is open and unfocused, and stays out of
the way when it is focused.
ESC FROM ANYWHERE. Separately and more simply: `dismissable` is false throughout sketch mode
because m_active is the FEATURE tool, so Esc fell through to whatever widget had focus. Click
any toolbar button or the Construction checkbox first and Esc did nothing at all — the likelier
reading of "Esc hardly ever works", and platform-independent. DesignCanvas exposes
request_sketch_exit() and the hook calls it whenever a sketch is live, after the inline-field
forwarding so an open field still takes Esc first.
RENAME. wxTR_EDIT_LABELS plus the two label-edit events write through to CadFeature::name and the
recipe, with a Rename verb in the offer and F2. The rebuild is deferred with CallAfter because
refresh_tree() destroys the very wxTreeItemId wx is holding during END_LABEL_EDIT — inline, it
killed the process.
STALE PICKS. set_tool now drops the Dimension tool's first pick, the Constrain picks and
m_point_sel, and delete_selected clears the pending dimension reference that could otherwise
dereference a renumbered entity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 39fac9b725. Parity re-verified: 17 files identical, 8 diverging by their
expected counts, DesignPanel.cpp still at 32 — the mirrored files were copied and the two
divergent ones patched hunk by hunk, so the counts returning to their expected values is the
proof each landed on the right side.
All 90 offer verbs are now firable by name over the socket, which matters because a deck key
can only send a keystroke and 49 of them have no shortcut at all. sketch_set_value calls the
same apply_dimension the in-canvas value field calls, so a typed dimension can be asserted with
no window manager in the way.
Three guards came with it, each confirmed against the source: on_mass_properties bounds-checks
m_sel_solid_body (it defaults to -1, and run_verb bypasses the menu grey-out that used to hide
that); sketch_set_value validates its value at the boundary because apply_dimension records a
driving constraint even for values it refused to apply; and run_verb refuses btn:/fly: verbs
that do not apply to the selection while leaving key: verbs alone, so the socket offers exactly
what the GUI offers. Dispatch is deferred through CallAfter so no modal verb can wedge the
socket thread.
GUI target builds and links against the rebuilt deps image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 971320e129, 6b049f0dc6, 4aae782029, 444d59f212, 74cf3d7e54 and the build
guards from 597557a6e4. Parity re-verified after every hunk: 17 files identical, 8 diverging by
their expected counts — DesignPanel.cpp still 32, DesignCanvas.cpp still 16, which is the proof
each hunk landed on the right side rather than being copied over a real divergence.
OFFSET OFFSETS THE CHAIN. Per-entity offsetting returned a closed rectangle as four parallel
segments that no longer touch, so entities_to_wires gave four OPEN wires and nothing could be
extruded. offset_entities now chains by shared endpoints and repairs each seam by mitering the
neighbours to their intersection. Second bug, invisible to any single-entity test: +d meant
"left of travel" for a line but "radius + d" for an arc regardless of sweep, so a slot outline
offset with its straights going one way and its caps the other. The convention is now written on
the declaration and pinned by a test.
tests/libslic3r/test_sketchprofile.cpp is new and asserts the LOOP rather than coordinates —
the property that decides whether a profile can be built, and the one the existing single-entity
[SketchEdit] cases cannot see. Its include is catch2/catch_all.hpp here: this fork ships Catch2
v3 while snaporca is on v2, which is why the test files are a tolerated divergence.
RIGHT-CLICK PICKS WHAT YOU POINTED AT, so a line's own verbs are offered instead of the
empty-selection vocabulary; sk_delete stops sharing btn:delete with the feature tree; and an
element's defining number (length / radius / diameter / angle / distance) can be typed, from the
menu or from V.
TWELVE MCP SKETCH VERBS. The socket had ~40 verbs and none touched a sketch, so the 2D layer
could only be exercised by driving a GUI with synthetic clicks. sketch_describe reports each
closed loop, the loops it encloses as voids, exact areas, and where a chain is still open;
sketch_validate/sketch_heal are FreeCAD's ValidateSketch — find vertices that overlap within a
tolerance but carry no coincidence, then weld them AND record the constraint, so a loop closed
by floating-point luck becomes one closed by construction. scripts/mcp-sketch-smoke.py is the
loop that asserts all of it.
Kernel suite on this fork: all tests passed, 2677 assertions in 230 test cases. The GUI target
links against the rebuilt deps image (the wxInspector blockage is gone) and the binary carries
the new verbs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Building orcacad-deps from this Dockerfile removes the snaporca-deps base, which is the point
(Trap 1: the old image's baked tree was project(Snapmaker_Orca)) — but that base was also where
Xvfb, openbox, xdotool and scrot came from. Without them gui-session.sh reports display DOWN and
orca-slicer dies with a trace trap on no display. Verified: the session now comes up with
display up, wm up, and the Untitled - OrcaSlicer window present.
Raises the bound from -j8 to -j12: the incident dump measured 1.17 GB average per cc1plus,
so 12 in flight is ~14 GB typical, well inside the 40 GB cgroup ceiling that is the real
guarantee. Keeps the file byte-identical to snaporca's copy, which the header requires.
Dockerfile.deps builds the deps with -j 12 for the same reason, and symlinks
deps/build/destdir -> deps/build/OrcaSlicer_dep: this tree installs under the latter name
while the orcacad_buildcache volume has the former baked as absolute paths in its CMakeCache.
Same tree, two names — without the link, one directory rename costs a full cold rebuild.
Both forks ran this script at once on 2026-08-21, each with ninja -j$(nproc)=16.
36 cc1plus held 42 GB of a 62 GB box, the kernel OOM-killed for 2h28m, ssh went
unreachable, lightdm was destroyed (2946 session kill events), and neither build
produced a single object file.
Three bounds, weakest to strongest: a flock on a path SHARED by both forks so
they serialise instead of summing; -j8 so the box stays usable while it
compiles; and --memory on the container, which is the actual guarantee — a
runaway build now dies inside its own cgroup instead of taking the host with it.
--memory-swap is pinned equal to --memory because swap thrash is what made ssh
hang rather than fail.
Kept byte-identical to the copy in the snaporca fork, as the header requires.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were mouse-only: Commit to Plate is a toolbar button bound to wxEVT_BUTTON,
the bed is a CheckBox, and neither had an accelerator. That put them out of
reach of anything driving the keyboard, and out of reach of a hand that had not
already left the model to find them.
Ctrl+Shift, because the Shift+letter space is full to the last letter and
because the char hook deliberately ignores every Ctrl-combo -- which is exactly
what leaves this layer free to claim. P is Plate and B is Bed; neither collides
with OrcaSlicer own Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate), and
nothing else in the tree binds either.
The lookup goes ahead of the guard that drops Ctrl-combos, and nothing already
bound changes meaning: a plain Shift+letter still resolves as before, because
the new layer only answers when Ctrl is held as well.
The bed toggle drives the checkbox rather than the viewport alone, so the
control and the view cannot disagree about what is shown, and it says which it
did in the status line.
Both verified on the running build: Ctrl+Shift+B toggles the grid and the
checkbox together, Ctrl+Shift+P commits to Prepare.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All 264 QD_* declarations re-derive from their family triples via
--remint Qidi. Only 8 family ids are new: most QIDI-brand products already
carried v3.2-minted ids on older printer series, so the QD-series
declarations converge onto them (QD_0..4_1_1 -> OFKIQO2W "QIDI PLA
Rapido"), and the six Generic families converge onto the shared generic
ids (Generic PLA -> OFDSrzZ8 etc.) by triple math -- no inherits or
compatible_printers changes anywhere.
--update-snapshot retires all 204 QD_* ids with mode-rule successors.
Audit: 204/204 non-null successors, every chain ends at a live id; the
four ids hardcoded in QidiPrinterAgent's non-numeric-series fallback
resolve to the OFL generics (QD_1_0_1 -> OFDSrzZ8, ...). The v4.1 agent
hook translates device-composed QD_* ids through these entries at
runtime, so box behavior is preserved on updated clients.
Known accepted residue (plan v4 SS4): three case/spelling family splits
keep distinct ids for one product line each -- QIDI PC-ABS-FR (series
1-2) vs QIDI PC/ABS-FR (series 3-4) vs Qidi PC-ABS-FR (X-Plus 4), and
QIDI TPU 95A-HF vs Qidi TPU 95A-HF. Renames are ruled out; the ledger's
per-series successors keep each device slot resolving to the right
presets, and a future upstream name unification self-heals through
content addressing.
The code<->ledger lockstep test is now active (un-skipped).
Gates: --check 0; orca_extra_profile_check 0/0; 117 script tests OK
(0 skipped); validator base, -f tree-wide, -v Qidi all exit 0;
libslic3r_tests green; Qidi profile diff is filament_id-value-only.
Qidi.json version bumped.
- is_island_declaration: only BBL remains an island; Qidi QD_* declarations
now enter the triple bookkeeping (checks 3 and 8, --remint's domain).
- reserved_space_owner: QD_* stays reserved but ownerless -- no vendor may
declare it, --update-snapshot refuses new QD_* ids outright, and vanished
QD_* ids take the retired-with-successor path instead of the island
release-with-hint path. New reserved_space_desc() names the space in
check 6 / sanction-gate messages.
- Check 1 drops the vendor-Qidi QD_* format exemption; --add-hint now
refuses QD_* keys (island space is GF* only); docstrings updated.
- Snapshot regenerated: the 204 QD_* declarer triples are now recorded
(declared triples 1113 -> 1317); ids/claims unchanged.
- Tests updated for the flip; new (skipped until v4.2b) lockstep test
asserting the QidiPrinterAgent fallback QD_* literals stay retired
ledger keys chaining to live ids.
Tree state is untouched: presets still declare their QD_* ids, all
grandfathered by the snapshot. --check exit 0; 117 script tests OK.
QidiPrinterAgent composes QD_<series>_<vendor>_<typeidx> setting ids from
device enums and previously degraded any slot whose id matched no visible
preset to generic-by-type. Plan v4.2 retires every QD_* preset id to the
succession ledger, so insert the ledger walk between the direct match and
the generic fallback: a composed QD_* id now forwards to its family's
minted OF* successor exactly like every other retired id.
Behavior-neutral until the ids are actually retired (the ledger holds no
QD_* keys yet, and live QD_* presets still match directly).
Also documents the code<->ledger lockstep on the hardcoded non-numeric-
series fallback table (QD_1_0_1/_11/_41/_50) and adds a C++ test pinning
that the succession walk is key-format agnostic.
Gates: libslic3r_tests [filament_id] 14 assertions green; libslic3r_gui
compiles.
* Add caching system for presets
* Removing user\bundle serialization and keeping it only for system presets
* Integrate caching into WebGuideDialog which speeds up time of SetupWizzard and PrinterSelection dialog
* Add CI\CD step to prepare cache file in ahead of time so user does not need to wait
* Add partial cache generation when only one of the vendros is changed to speed up recalculation time
* Handle corrupted files
* Add cache to GuideDialog as previos version didn't work as expected
* Add inspecting tool and fix CI cache generation
* Generate cache per vendor
* Simplify code by mergin it in PresetBundle
* Simplify code a bit more
* Add cereal serialize() to VendorProfile, PrinterModel, Preset, and Semver
* Remove CachedPrinterModel/VendorProfile/Preset mirror structs from VendorCache
* Fix use-after-free in CallAfter lambda; replace raw thread pointer with unique_ptr
* Use get_vendor_cache_key() to match cache keys written by the app
* Remove BOM added by VSC
* Skip invalid vendors
* Remove leftover cache file
* Fix build for windows arm64
* Revert json cache back
* Update check for stale cache
* Serealize all value fields for Preset class to minimize regression later
* Minimize field duplication by moving Cache thing into PresetBundle
* Add tests for Cache system
* Add a bit more tests
* Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed
* Rvert from per-verndor to single cache file
Replace N per-vendor .cache files with a single system_presets.cache
that holds all vendors and presets in one serialized blob.
Cache load is now all-or-nothing: on hit all vendors are applied from
the bundle (sub-second); on miss all vendors are parsed from JSON and
a fresh bundle is written to the user cache dir.
Invalidation is driven by bundle_key - a sorted concatenation of all
vendor JSON version strings. Any vendor update invalidates the whole
cache and triggers re-parse on next launch.
Guide wizard (WebGuideDialog) loads the bundled cache into a plain
PresetBundle instead of a separate VendorGuideData struct, removing
the duplicate data model.
generate_system_cache simplified from a per-vendor loop to a single
save_system_presets_cache() call producing one output file.
* Transfer all Preset fields from cache via move assignmet
apply_vendor_preset_group was copying fields manually and missed
bundle_id, user_id, base_id, sync_info, updated_time, key_values,
ini_str. Replace field-by-field copy with move assignment of the
fully-deserialized Preset, then restore the vendor pointer which
is excluded from serialization.
* Ignore cache for future
* Remove not used files
* Ship one preset cache per vendor in place of the profile JSONs
Each vendor's system presets serialize into a single <vendor>.opc built at
package time, and a shipped build carries that file alone — the profile JSON
and its sub-file tree are pruned. The vendor loader, the setup wizard's profile
list and the resource installer all read a vendor through its cache, falling
back to parsing whenever one is absent, stale or unreadable, so the cache stays
an optimization and never a source of truth. Caches hold presets in source form
and resolve inheritance at load, through the same code the JSON path uses.
* Make the preset cache self-describing and load each vendor from the system folder alone
The cached DynamicPrintConfig is keyed by name, through a per-file dictionary of the
distinct opt_keys, the type each was written as, and the distinct enum value names,
instead of by serialization_key_ordinal — a position assigned by declaration order at
static init, where inserting one option shifts every later ordinal and the lookup then
succeeds on the wrong option. Because a name-keyed payload drops the options this build
cannot place rather than being rejected wholesale, the schema fingerprint goes, and with
it the two fallbacks that existed only because an installed cache died on every app
upgrade: the second lookup tier into resources/profiles and the parse fallback to the
same place. A vendor is loaded from <data_dir>/system/ and nowhere else, as on main —
which is what makes the app write its .opc files there again.
* Simplify the preset cache internals after review
* Use the shared temp-dir helper in the preset bundle loading test
* Bound stamp string reads in the preset cache
* Speed up the setup wizard with a profile-data cache
The wizard's per-vendor fast path threw on vendors present only in
resources, falling back to a ~29 s raw JSON scan on every open. Each
vendor now loads from the directory it was found in, and the derived
model/machine/filament/process catalog is cached whole in
<data_dir>/cache/wizard_profile_data.json, stamped by each vendor's
name and version - a fresh cache makes an open one file read, with no
bundle built and no presets installed (~0.2 s vs ~2 s).
* Remove debug SVG dump from a geometry test
* Move the per-vendor cache file format into PresetCacheFormat
* Move the vendor install helpers from PresetBundle into Utils
* rename
* fix flatpak
* change cache version to 1
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Normalize the junction direction vector over XYZE
calc_vmax_junction_deviation() treats the dot product of two jd_unit_vec as a
cosine, but the vectors were scaled by 1 / block.distance, which is the XYZ
length. On an extruding move the E component then pushes the 4D norm above 1 and
the dot product below -1, so the corner reads as straighter than it is and is
planned too fast -- the more so the higher the flow. Measured on a 6 degree
corner at scv 5: 86.9mm/s with no extrusion, 94.4mm/s at 0.029mm/mm, 150.0mm/s
at 0.1mm/mm.
Neither firmware does that. Marlin normalizes over XYZE for any extruding move
(planner.cpp: `if (... || esteps > 0) normalize_junction_vector(unit_vec)`) and
Klipper leaves E out of the cosine entirely, dotting only axes_r[0..2]
(toolhead.py::Move.calc_junction). Normalizing satisfies both: with E normalized
in, the cosine differs from the XYZ-only one by ~1e-5 at printing flow rates.
This is a deliberate divergence from PrusaSlicer, which still scales by
1 / distance -- it carries an older Marlin's behaviour.
Travel moves are unaffected, their vector was already unit length.
Reported by Copilot in review of #15304.
* Test that extrusion rate does not change corner planning
The junction deviation tests were all travel-only, which is exactly why the E
component of the junction vector went unchecked. Cover it: the same corner has
to be planned the same whether nothing, an ordinary 0.42 x 0.2 line, or a fat
large-nozzle line is extruded through it, on both Klipper and Marlin 2.
Reported by Copilot in review of #15304.
Reported on PR #15238: on macOS, drawing a corner rectangle and pressing Enter
on the first dimension opens the second field and then wedges the entire
application — no typing, no Escape, dead menu bar, no tab switching, and the
field stays composited over the desktop after minimising. That last detail is
what identifies it: an already-drawn window keeps being composited by the
WindowServer once the process stops answering. The main thread is stuck.
4c8c93b512 is the only commit that ever changed the second-queued-field path.
It stopped unmapping the value field between two queued dimensions, and its
whole justification is mutter: focus-stealing prevention refuses keyboard focus
to a window that was just re-mapped, which left the second field visible but
dead on GNOME (snaporca-p8uw). It was applied with no platform guard, so macOS
re-activates an already-visible borderless NSWindow at NSModalPanelWindowLevel
from inside wxOSX's pending-event drain — which on macOS runs from a
CFRunLoopObserver at kCFRunLoopBeforeTimers (evtloop_cf.cpp) over an unbounded
`while (!m_handlersWithPendingEvents.IsEmpty())` (appbase.cpp).
One constexpr now carries that choice, and both halves of the contract read it,
because the failure mode of this fix is the two halves drifting apart: open()'s
reuse-if-shown and do_commit()'s deferred hide are one decision, not two.
Not a macOS guess I could not check: the non-GTK branch was exercised on the
Linux rig by forcing the constant to false and rebuilding. A corner rectangle
drew, its first field committed at 60 mm, the SECOND field opened, committed at
40 mm, and the sketch ended at 60.0 x 40.0 mm with the field closed and no
freeze — so the map-afresh path is functionally complete, not merely different.
On GTK the constant is true and every generated instruction is unchanged.
What is still unproven is the exact line where macOS wedges; the reporter has
been asked for a `sample` of the hung process. This fixes the cause the evidence
points at without waiting for that, and cannot regress the GTK behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review request on PR #15238: "Place CAD-related files (e.g. CadDocument/
GeometryEngine) into a separate folder."
src/libslic3r/CAD/ the kernel — CadDocument, GeometryEngine, the four
Sketch* units, SketchSolver, ThreadStandards
src/slic3r/GUI/CAD/ the tab — DesignPanel, DesignCanvas, DesignSketchTool,
SketchInlineEditor, McpControl, generated DesignOffer
Pure relocation: no line of logic changes. Two include rewrites follow from it —
files that moved re-spell their own neighbours against src/ (already on the
include path), and files that did not move pick up the new folder. docs and
docs/ux/mockups/gen_offer_table.py follow the same paths.
Verified: libslic3r, libslic3r_gui and libslic3r_tests all build, CAD suite green
at 2518 assertions in 194 test cases, and the sibling fork builds identically —
17 shared sources still byte-identical, 8 diverging by their expected counts.
Review request on PR #15238: "Move the SLVS to deps if the source code remains
unmodified. If any changes were made to the source code, move it to deps_src."
It is unmodified — all 20 files under src/libslic3r/slvs were byte-identical to
JacobStoren/SolveSpaceLib@4d87045, the extraction of solvespace.com's libslvs.
So deps/ it is, fetched by hash like every other dependency.
Only the CMakeLists is ours: upstream's builds a demo executable and installs
nothing, so deps/SLVS/CMakeLists.txt.in replaces it via PATCH_COMMAND — the same
shape deps/OpenCSG already uses. The public header keeps its spelling, so
SketchSolver.cpp still says `#include <slvs.h>` and needs no edit.
The CI deps cache is keyed on hashFiles('deps/**'), so it rebuilds itself.
Verified: dep_SLVS builds and installs, libslic3r links against SLVS::slvs, and
the CAD suite is unchanged at 2518 assertions in 194 test cases.
* Plan corners with junction deviation where the firmware uses it
The time estimator only ever had the classic per-axis jerk model, which limits a
corner by the largest single-axis component of the velocity change. That is
anisotropic: the same corner is allowed sqrt(2) more speed on a diagonal than on
an axis, which paints a four-lobed ripple around every circular wall in the
actual speed and actual flow views, worst on small parts whose walls are made of
short segments.
Klipper has no classic jerk at all and Marlin 2 has none while M205 J is in use;
both plan corners with junction deviation, which sees only the corner angle. Add
that model and use it for those machines:
- Klipper: derived from the square corner velocity, as the firmware does
(jd = scv^2 * (sqrt(2) - 1) / max_accel), reading the scv from
machine_max_jerk_x, where process_SET_VELOCITY_LIMIT() already stores
SQUARE_CORNER_VELOCITY.
- Marlin 2: machine_max_junction_deviation, which was already loaded into the
machine limits but never reached the planner.
- Every other flavor keeps the classic jerk path unchanged.
The model has no per-axis jerk floor, so this also drops the hard slow spot the
estimator drew at the start of every loop from machine_max_jerk_e.
Toolpaths are unaffected: on a full export the only lines that change are M73.
The junction deviation maths, including Marlin's JD_HANDLE_SMALL_SEGMENTS arc
approximation, is ported from PrusaSlicer's src/libslic3r/GCode/GCodeProcessor.cpp.
The Klipper mapping is not in PrusaSlicer, which ignores SET_VELOCITY_LIMIT.
* Add tests for junction deviation corner planning
Cover the three properties the change rests on:
- a right angle on Klipper is planned at exactly the square corner velocity,
the identity that makes the scv to junction deviation mapping correct, and a
shallow corner is planned far faster than per-axis jerk allows;
- junction deviation gives the same speed whatever the corner's orientation,
while classic jerk keeps its sqrt(2) spread, which is the four-lobed ripple;
- machines that do not plan with junction deviation are provably untouched,
including a Marlin 2 printer that has it disabled.
# Description
Printers discovered/bound under one printer agent (e.g. built-in BBL)
were leaking into another, independent agent's "My Device"/"Other
Device" lists and inheriting its saved access code, since neither the
device list nor bind state was ever scoped by which agent found them.
- Add printer_agent_id to MachineObject/BBLocalMachine, stamped at
discovery/bind time; filter get_my_machine_list(),
get_my_cloud_machine_list(), and update_other_devices() by it.
- clear_other_devices() now drops entries stamped by the outgoing
agent on swap, so the incoming agent's own discovery re-inserts and
re-stamps them fresh instead of leaving them stale-tagged forever.
- Scope access_code by (dev_id, printer_agent_id) on BBLocalMachine
(LAN only since cloud's userMachineList is always refreshed live from
the account API, so it isn't at risk the same way), with a
BBL-only legacy fallback to the old flat access_code/user_access_code
keys so existing bindings keep working.
# 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.
-->
<!--
> 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)
# Description
Break the recursive include dependency by moving GUI_App.hpp from
HMS.hpp into HMS.cpp. Add the required standard headers and use explicit
std/nlohmann types to remove reliance on transitive includes.
This prevents recursive header inclusion while preserving HMS
functionality.
# 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.
-->
<!--
> 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)
# Description
This PR enables building OrcaSlicer on Windows with the **clang-cl
(LLVM)**
toolchain and the **Ninja** generator, in addition to the existing MSVC
path.
Clang is already supported on Linux with this codebase, so this extends
that
support to Windows.
The changes fall into three categories. All are either no-ops on the
existing
MSVC/Visual Studio path or Windows/clang-cl-specific, so the standard
build is
not affected.
### 1. C++ conformance fixes
clang-cl is stricter than MSVC and rejects several constructs that
cl.exe
silently accepted. Each of these is non-conforming code that MSVC
tolerated:
* **Explicit template instantiation in `BoundingBox.cpp`** — clang-cl
does not
instantiate `BoundingBoxBase<Point, Points>::construct` through the same
transitive path MSVC uses; added the explicit instantiation.
* **Eigen cast materialization in `AABBTreeLines.hpp`** — `.cast<T>()`
returns a
lazy `CwiseUnaryOp`, not a concrete `Matrix`; materialized it before
passing to
`distance_to_squared`, which expects a concrete type.
* **`LabelItemType` underlying type in `PresetComboBoxes.hpp`** — gave
the enum
the same `std::size_t` underlying type as `Marker` to resolve a
narrowing
conversion in a switch.
* **`T2A_` cast in `BaseException.cpp`** — explicit `static_cast<const
char*>` on
the ATL conversion helper result.
* **Wide string literals in `GUI_App.cpp`** — used `L""` literals where
concatenated with a `std::wstring` (`url_prefix`).
### 2. Build system / dependencies
* **Exclude clang-cl from MSVC-only CMake guards** — `if(MSVC)` is true
for
clang-cl, so blocks applying cl.exe-only flags now exclude Clang.
* **Disable TBB LTCG** — oneTBB enables MSVC IPO/LTCG by default,
emitting
proprietary `/GL` bitcode objects that `lld-link` cannot consume.
Disabling IPO
produces native COFF, linkable by both `link.exe` and `lld-link`. TBB is
threading infrastructure, so the runtime impact of disabling LTCG is
negligible.
* **wxWidgets target path** — clang-cl uses the MSVC frontend variant on
Windows
but reports compiler id `Clang`, so wxWidgets looked under
`clang_x64_lib`
instead of the `vc_x64_lib` layout the deps are built with.
### 3. Ninja generator support
* **Runtime DLL copy** — the DLL copy step was nested under
`CMAKE_CONFIGURATION_TYPES` (multi-config only), so single-config Ninja
skipped
copying OCCT/GMP/MPFR/WebView2/freetype DLLs next to the executable. Now
runs
for both generator styles, guarded by `if(WIN32)`.
* **`build_release_vs.bat` Ninja target** — `ALL_BUILD` is a Visual
Studio
target; Ninja uses `all`. The script failed with
`ninja: error: unknown target 'ALL_BUILD'` when invoked with `-x`.
## Tests
Built from a clean checkout on Windows with:
* clang-cl 22 (LLVM toolchain bundled with Visual Studio 18)
* Ninja Multi-Config generator
* lld-link as the linker
The full build (deps + slicer) compiles, `OrcaSlicer.dll` links with
`lld-link`,
and `orca-slicer.exe` runs. Verified end-to-end by loading a model,
slicing it,
and generating valid G-code (screenshot below).
The existing MSVC / Visual Studio build path is unaffected — all changes
are
guarded by compiler/generator/platform checks or are conformance fixes
that
compile identically under MSVC.
# Screenshots
<img width="1919" height="1079" alt="image"
src="https://github.com/user-attachments/assets/02082437-db36-4696-a91a-d9acf57d4a52"
/>
Selecting the web Device tab loaded the printer's web UI from the selected discovered machine
when the preset carried no host. That arm was lost merging main into this branch — two of the
three Plater.cpp hunks from #15134 survived, this one did not — leaving the tab blank, since
PrinterWebView starts on an empty URL and nothing else navigates it.
In printer-agents mode the legacy web page was appended under Notebook::PAGE_MONITOR, which
resolves to the same "monitor" id as the native Device tab. FindPageByName returns the first
match, so PluginPages::relayout() — which saves the selection by name and restores it after
rebuilding the tab strip — moved the user off the web tab onto the native one. The tab also
disagreed with its own label, being created as "Device (legacy)" and renamed to "Device (Web)"
on the next show_device() call.
Per material slot, the Publish dialog can now embed the entire filament preset ("Full Publish") and require a curated filament type and/or colour:
- On export, full-publish vector options are masked to the author's slot so unrelated slot data never leaks into the published file.
- On load, slots are matched by the published type: a match keeps the receiver's material (full dumps ignored, partial keys applied); a mismatch replaces the slot with the first visible same-type library filament, falling back to a temporary embedded preset or skipped keys when none exists. Required colours apply regardless of the type match.
- The receiver's slot count grows only to the highest published slot.
- Published 3MFs load as a new project: the file's path is not adopted as the project filename, published metadata is stripped from the model, and the file is added to recent projects.
- Notifications list replaced slots, and the edited filament preset is refreshed so applied values surface in the GUI.
- Dialog: "Full Publish" toggle replaces the material opt-in and select-all headers; new Color/Type requirement rows with swatches.
- Add Ctrl+Shift+E shortcut for the Publish dialog (menu, key handling, and the keyboard shortcuts dialog).
- Tests for export slot masking, metadata round-trip, replacement semantics, slot growth, and skipped-key reporting.
* Add /bot merge for delegated vendor profile maintainers
Vendor profile PRs no longer need a maintainer with repository write access: an
account listed in the FOLDER_MERGERS variable can squash-merge a PR confined to
the folders it owns by commenting /bot merge on it. Anything reaching outside
that grant, targeting a branch other than main or release/*, or missing a green
Check profiles run is declined with a comment naming the offending files.
Grants live in the merge-delegation environment, so only an admin can change who
may merge, and MERGE_BOT_DRY_RUN stops all merging without a code change.
Check profiles now also runs on release/* pull requests; nothing else changes
for existing contributors.
* Add profile version bump to the code review checklist
Without the bump in resources/profiles/<Vendor>.json, a preset change never
reaches existing installs over the air.
The last unbuilt element of snaporca-wgsc. Two connectors a mate binds are one
object with a gap still in it; drawn as two separate frames they read as
unrelated, and "which two of these five frames are the mate?" had no answer on
screen at all.
Dashed, grey, drawn IN WORLD along the segment joining the origins -- so it
foreshortens with the model and its length is the gap the mate has left to
close. A Fastened mate therefore draws nothing, which is correct: the gap is
zero. Screen-constant dash pitch (6 px dash, 4 px gap) like every other gizmo
here, with the pitch opening up beyond 400 dashes so a mate across a large
assembly cannot emit thousands of segments. Depth test off: the line's job is
to say "these two belong together", and it has to say it even when a part sits
between the camera and one end.
Fed from BOTH sources of polarity truth, the same two the role colours already
use: every committed Mate feature (connectors named by feature index), and the
live pick of an open Mate card (named by combo ordinal). Only resolved frames
are eligible, so an unresolved end draws no line rather than a line to the
origin of the world.
RIG-VERIFIED on :10 with the fresh binary (/OrcaSlicer/build/src/Release,
2026-08-17 12:43): two imported bodies, a face-and-direction connector on each,
Planar mate offset 40. Sampling the segment between the two origins gives a
regular dash/gap alternation of 13:10 sample units -- the 6:4 px pitch -- in the
stroke grey (107,117,133), over both solids. The grey end keeps its open collar
head and the blue end its filled one, so polarity and pair now read together.
Refs snaporca-wgsc
Tommaso's decision (snaporca-x0kd): face orientation is hardwired perception -- a toddler reads
a face's roll and verse with no instruction -- so the connector is a face by default and the
conventional disc + roll quadrant stays, selectable, for users who expect it.
Preferences > Control > Camera > "Draw mate connectors as a face", default ON, key
design_connector_face_glyph. Read every frame rather than latched, so toggling takes effect on
the next repaint -- a look you cannot A/B without restarting will not get compared. Verified on
the rig: unchecking it switches the viewport to the disc live, no restart.
WHY A RELIEF AND NOT A DRAWING. A flat face in the connector's plane foreshortens by
sin(elevation) and collapses at a grazing view exactly like the quadrant it replaces -- measured,
the quadrant falls 89 -> 20 -> 3 -> 0 lit pixels from 47 degrees to edge-on. The relief does not:
its silhouette carries the information. So the glyph is a small shaded solid, painter-sorted,
lambert-shaded against a light fixed in CAMERA space so orbiting does not swing the shading.
THE MUZZLE, AND THE MISTAKE THAT NEARLY LOST IT. It is the only feature standing along +Z, so it
says which way the connector points and it is all that survives edge-on. Two errors on the way:
1. I built its footprint from height*tan(draft) and got a needle. The real base OVERHANGS the
crest at both ends (0.062 nose, 0.034 tail) and that overhang is what makes it a wedge. Base
now lifted straight off the mesh.
2. Worse, I chased fidelity. Scaled honestly the ridge is 11.3 mm on an 83.3 mm face -- 13.6 %
of the width -- and at 22-48 px that is a scratch. Tommaso looked at it and could not find
the muzzle at all, which is the only test that counts. A glyph is a symbol, not a scale
model, so it now gets two deliberate exaggerations, and COLOUR does most of the work:
muzzle share of lit pixels at 90/16/6 deg -- body tone 14.8/11.3/17.5 %, accent gold
18.3/19.2/23.9 %, accent gold at 1.8x width 23.5/25.2/31.2 %.
The accent is the same gold the disc spends on its roll quadrant, so it stays this tab's "here
is the direction that matters" colour. Polarity is still on the Z arrow's head; nothing collides.
A connector whose ROLL COULD NOT BE DERIVED keeps the disc treatment whatever the preference says.
A face asserts a definite orientation, and asserting one for a roll that was never derived is the
same confident lie that got billboarding rejected.
Geometry is emitted from the part by docs/design/mate-connectors/emit_glyph_table.py, not
hand-drawn, so glyph and printed connector cannot drift: 12-vertex outline, two eyes, chin bar,
cheek dot, and the snout wedge. Crest 29.0 mm / 6.58 mm drop / 13.1 deg against the review's
28.3 / 6.61 / 13.1 on the B-rep.
Also fixes extract_outline.py, which walked w.Edges: OCC returns them in storage order, not ring
order, ignoring per-edge orientation, so the outline was scrambled -- 45 points and perimeter
6.380 where a clean ring gives 31 and 3.335. Every measurement in the design notes was re-run.
The correction reversed one earlier finding: handedness does NOT read on its own (5.4/8.0/9.1 %
different from its mirror, not the 32-35 % the scrambled ring produced), so the cheek dot is
required rather than merely nice.
RIG-VERIFIED on Xvfb :12 against a 60x40x10 box with a face+edge connector: the face renders with
both eyes, ears, chin bar, cheek dot and a gold muzzle standing proud; the Z arrow degenerates to
its ring when viewed down the axis; and the preference switches to the disc live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The connector work has lived outside the code since 2026-08-05, in a workspace repo with no
remote. It is the basis of a decision that now shapes the Design tab, so it belongs here.
docs/design/mate-connectors/
DESIGN_MATE_CONNECTORS.md seven CAD systems surveyed; the frame-pair model this kernel
already matches; sections 8b/8c on the glyph, and section 9's
four open decisions (D1-D4) still awaiting Tommaso.
bear.step the male, Onshape 2026-08-05T08:27Z, md5 faf228326ee3f971
BearConnector_Female*.step/.stl, BearConnector_Cutter.step
built by make_female.py FROM the real male B-rep rather than
re-modelled, so the pocket is complementary by construction
including every deliberate asymmetry. Fit measured at exactly
0.2000 mm, zero interference, mated hosts proven coplanar.
BEAR_CONNECTOR_REVIEW.md the symmetry-group result: identity 81/81 edges, mirror-x 0/81,
mirror-y 0/81, rot180Z 0/81, rot90Z 0/81, diagonal 0/81 at
0.1 mm. Trivial group, so every PARTIAL view fixes orientation.
extract_outline.py, simplify_study.py, relief_sheet.py, handedness.py, make_female.py,
trim_female.py, fit_check.py, verify_trimmed.py, coplanar_test.py + their sheets
THE DECISION THIS SUPPORTS (snaporca-x0kd): the mate connector is drawn as a simplified BEAR
FACE by default, with the standard disc + roll quadrant + Z arrow kept behind a preference.
Face orientation is hardwired perception -- a toddler reads a face's roll and verse with no
instruction -- and no abstract glyph earns that. Measured against the alternative: the disc's
gold quadrant+tick falls 89 -> 66 -> 37 -> 20 -> 3 -> 0 lit pixels as the camera drops from
47 deg to edge-on, and is a shapeless blob by 16 deg.
WHAT THE SIMPLIFICATION STUDY SETTLED (snaporca-wi3z), all measured off the real B-rep:
The eyes are load-bearing. Same outline and muzzle with the eyes removed stops reading as
a face at every size. Whatever else goes, they stay.
45 -> 22 outline vertices with no loss of read at 22 / 32 / 48 px; the muzzle reduces to
one filled triangle. Three marks plus a cheek dot.
Drawn FLAT the face fails exactly where the disc fails: in the connector's plane everything
foreshortens by sin(elevation). Rendered as its real relief instead, lit pixels at 32 px go
164 -> 210 at 16 deg and 69 -> 120 at 6 deg, and the snout ridge stands proud as a profile
rather than smearing. The glyph must be a shaded relief, not an outline.
Handedness already reads without any added mark -- 32 to 35 % of lit pixels differ from the
mirror, and re-registering by best whole-pixel translation returns offset (0,0), so it is
real shape asymmetry. But it reads only BY COMPARISON. A dot on one cheek makes it local:
34.5 / 37.0 / 36.4 %, and unlike uneven eyes (42 %) it does not read as a defect.
Tommaso's calls: it stays a bear, and handedness must read.
The scripts were repointed at the co-located male and extract_outline.py re-run from here to
prove it -- same 45 outline points, same three inner wires, same 3829.5 mm2 back plate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
snaporca-wm4s. Thickening the 4-walled open box (60x60 in plan, 40 tall, no caps)
by 5 produced volume 29648.15 where the geometry requires (60^2-50^2)*40 = 44000
— about 67% of it. The corner material at the four vertical edges was simply
absent.
CAUSE. MakeThickSolidBySimple offsets each face along its own normal and sews;
it never extends neighbours to meet, so wherever two faces join at an angle the
corner is empty. A flat sheet has no such join and was always exact (18000.000),
which is why the defect looked like a measurement artefact.
WHY THE TWO EARLIER ATTEMPTS COULD NOT HAVE WORKED. Both switched to ByJoin —
plain, then with Intersection/GeomAbs_Intersection — and both returned a shell,
not a solid, so the body lost its volume entirely and both were reverted. That is
not a parameter problem: in OCCT, BRepOffset_MakeOffset::MakeThickSolid builds a
solid only inside `if (!myFaces.IsEmpty())` (BRepOffset_MakeOffset.cxx:1115).
Handed an open sheet with no closing faces, it stops after the offset shell and
returns it, reporting IsDone() with a non-null shape containing no TopAbs_SOLID.
ByJoin hollows a CLOSED solid by removing faces; an open sheet is outside its
contract.
FIX. Close the sheet, then use the call that mitres: cap the free rims
(ShapeAnalysis_FreeBounds -> MakeFace), sew shell+caps into a closed shell, make
a solid, and hollow it inward passing the caps as the faces to remove — the caps
come back off and leave the wall. Two details, each found by measurement rather
than reasoning:
* A shell sewn from an extruded sheet carries no guarantee of outward
orientation, and MakeSolid does not fix it. Inside-out, the inward offset goes
OUTWARD: measured bbox 70x70x40 and volume 339141.59, larger than its own
bounding box because the result overlaps itself. A negative GProp mass is
exactly that inversion, so it is the test; Reverse() on it.
* A SINGLE face has no neighbour to mitre and must keep the BySimple path. It
does have a free boundary, so "has free wires" is the wrong question — capping
a lone face with its own rim sews a zero-thickness shell and measures 6000
against 18000.
Also: IsDone() is not a success test here, since both failed attempts had it
true. The code now explores for TopAbs_SOLID and refuses a shell.
Tests: new case asserts 44000 with the wall's bbox at 60x60x40 (catching the
inverted-orientation shape, which has the right volume nowhere near the right
place), plus the flat-sheet control at 18000 that must not regress. Full kernel
suite green: 2502 assertions in 187 test cases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, found by driving the app: clicking Login /
Register on a fresh install pops "You are currently in Stealth Mode. To log
into the Cloud, you need to disable Stealth Mode first." — to a user who has
never touched the toggle, whose config says stealth_mode: false.
It is the same pre-wizard latch one step earlier. handle_web_request() gates the
login commands on get_stealth_mode(), which reports stealth while
firstguide/finish is unset, so the app blocks sign-in because the wizard is
unfinished — backwards, since signing in is how a user leaves that state.
Worse, the escape it offers does not work. "Quit Stealth Mode" writes
stealth_mode = false, which was ALREADY false, and never touches the latch: the
config is byte-identical afterwards and get_stealth_mode() still returns true.
The user clicks the button, believes stealth is off, signs in, and finds every
cloud feature still dead. That is the state the reporter of #15239 described.
So the login guard now reads the user's OWN setting via the new
get_stealth_mode_setting(), not the pre-wizard default. A user who deliberately
enabled Stealth mode still gets the dialog and the working Quit button; a user
who merely closed the wizard goes straight to the login page.
Measured on Xvfb with a fresh datadir (firstguide absent, stealth_mode false):
before, clicking Login produced a "Stealth Mode" window; after, it opens the
"Login" window directly. And with the previous commit's latch release, a real
Orca Cloud sign-in on that same unfinished-wizard profile now runs the whole
post-login flow — the sync prompt fires (sync_user_preset lands in the config),
the per-user preset folder is created, and Sync Presets syncs with no refusal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported on #15239: after signing in to Orca Cloud on a fresh install, no sync
prompt appears and File > Sync Presets is greyed out with nothing to explain why.
CAUSE. AppConfig::get_stealth_mode() returns true whenever `firstguide/finish` is
unset, and that flag is written in exactly one place — GuideFrame::SaveProfile(),
i.e. only when the setup wizard is COMPLETED. Closing the wizard is what people do
today to reach the login (the wizard never offers it, which is #15239 itself), so a
new user ends up permanently in a stealth mode they never chose. Every cloud gate
keyed on get_stealth_mode() then switches off silently, including:
* GUI_App::on_user_login_handle(), which returns EARLY on stealth — so the whole
post-login flow is skipped: preset migration, plugin fetch, user-preset load and
show_sync_dialog(). That is the missing sync prompt.
* the Sync Presets item in both the top menu and the File menu, whose enable
lambda was `is_user_login() && !get_stealth_mode()`. That is the greyed item.
The result is indistinguishable from real Stealth mode, and nothing in the UI says
so, because the one place that DOES explain it — the "Quit Stealth Mode" dialog in
handle_web_request() — only covers the homepage login commands.
FIX, two parts.
1. The pre-wizard value is a DEFAULT for "the user has not been asked yet", not a
setting, so it must not survive the user answering. Signing in to a cloud account
is that answer. AppConfig now carries a session-only `m_cloud_logged_in` mirrored
from the network agent (on login, on logout, and at agent start so a restored
session counts), and get_stealth_mode() consults it before falling back to the
pre-wizard default. An explicit Stealth mode setting is untouched and still wins:
a user who turned it on deliberately stays offline whether or not they sign in.
2. Sync Presets no longer greys itself out. Both refusal paths already had a message
to show — "You must be logged in…" and now one for Stealth mode naming the
Preferences toggle — and the enable lambda was making both unreachable. A disabled
item that cannot say why is the reason this took a bug report to find.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
snaporca-x1k7 filed three Transform defects. Two were real and are fixed here;
the third does not reproduce and is withdrawn with its measurement.
(1) The feature tree had no ITEM_ACTIVATED binding at all, so double-clicking any
row only highlighted it. Double-click is the documented edit gesture elsewhere
(a committed sketch opens that way on the canvas), which made every feature look
dead until the user found the Edit button in the section header. Bound to
on_edit_feature(), so the gesture now works for every feature type, not just
Transform.
(3) The Transform card's typed fields called refresh_preview(), but preview_fields
returns {} for Transform and the solid-preview path has no ghost to build for a
feature that moves an existing body — so typing a distance changed nothing on
screen until Confirm. The fields now drive the same channel the gizmo drag already
uses: the body's display transform. In EDIT mode the committed transform is
already baked into the kernel geometry, so the preview undoes it first; without
that term, re-opening a committed Z=20 and typing 40 would show the body at 60.
(2) NOT REPRODUCED. Dragging a rotation ring does fill the field: a tangential
drag on the red ring gave Rotate axis = X, Angle = 27.44 deg, plus the translation
that rotating about the card's pivot implies (Y 17.60, Z -62.11). The original
reading came from a drag that never grabbed the 7 px ring; this run took its
candidate points from the rendered ring pixels themselves and 6 of 6 answered.
Rig-measured (docker snaporca-gui, Xvfb :10, llvmpipe), vertical screen shift of
the body by image correlation:
commit Translate Z 0 -> 20 : +140 px
double-click the Transform row : +0 px, and the card re-opens showing 20.00
step the re-opened card 20 -> 40 : +142 px (not +280 -> the undo term is right)
Cancel : +0 px vs the committed frame, residual 0.46
Add mode: stepping Z moves the body immediately (viewport diff bbox
200,143-1181,999); Cancel puts it back with only the status strip differing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The static libavcodec.a/avutil.a compiled the auto-detected
videotoolbox/audiotoolbox objects, which reference VideoToolbox
framework symbols (_VTDecompressionSession*). The app link line
happened to satisfy them transitively, but the orca_stubgen module
link (CI-only) failed with undefined symbols. The player decodes in
software (swscale), so disable both HW-accel paths to keep the
static libs self-contained.
Co-Authored-By: Claude <noreply@anthropic.com>
On a Wayland session the in-canvas value field never took the keyboard focus, so
after drawing a rectangle the first keystrokes went nowhere and the field had to
be clicked before a number could be typed. open() already did Show, Raise,
SetFocus on both frame and control, SelectAll, and re-asserted all of it in a
CallAfter — none of it works here, and no amount of re-asserting would: under
Wayland a client cannot focus itself, and mutter ignores gtk_window_present()
without an activation token as focus-stealing prevention. The earlier fix
recorded in this file (dropping wxFRAME_FLOAT_ON_PARENT, whose GTK _UTILITY_
hint made an xrdp session refuse focus) addressed a different compositor.
Stop needing WM focus. The canvas keeps the focus and feeds the field:
SketchInlineEditor::feed_key() types into the control directly — Enter commits,
Esc cancels, Backspace/Delete edit, digits and '-' '.' ',' are accepted, and
anything else is handed back so a stray letter cannot vanish into a numeric
field. A m_fresh flag reproduces the SelectAll semantics the field already had,
so the first digit replaces the prefill. It returns false when the control
genuinely holds the focus, so X11 keeps wx's normal routing and no character is
typed twice.
The CHAR_HOOK gates on the editor's own is_open(), NOT on inline_busy().
inline_busy is a freeze flag for the sketch tool: cleared on commit, re-set only
when the next queued field opens, with a CallAfter between them. Gating on it
left a window where the field was on screen and the flag was false — typing
worked for a rectangle's Width and not its Height.
VERIFIED at the machine on behemoth: typing the first dimension directly, with
no click, works. NOT yet confirmed: the Width -> Height handover; the is_open()
gate is diagnosed from the handover code, not observed. The hook's
SNAPORCA_KEYTRACE=1 switch logs each key with the focused widget if it needs
chasing further.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Select a line in a sketch, right-click, and every sketch verb was greyed: Trim,
Extend, Fillet, Chamfer, Offset, Mirror, the arrays, Constrain. The menu was
right and the selection was gone — two independent faults, each of which hid
the other.
FIRST, the Select-mode RightDown branch called clear_selection() before handing
the click back. Handing it back is correct: the m_right_consumed flag means "the
tool USED this right-click", and a plain right-click is not a gesture
terminator, so the offer should open. Clearing first is not: the offer describes
WHAT IS SELECTED, so wiping the selection guaranteed it could only ever describe
nothing. Deselection keeps its own gesture — left-click on empty space, a few
lines above in the same handler.
SECOND, offer_selection_kind() returned SkNone for every sketch state. The offer
table has always carried verbs for a selected line, arc, point or pair, but
nothing ever RETURNED those kinds, so fourteen rows were gated on selection bits
no code path could set. Classify the selection instead: SkLine / SkArc / SkPoint
/ Sk2Ent, via a first_selected_type() accessor on the tool and two forwarders on
the canvas.
Either fix alone measures as a failure — the classification is handed an empty
selection, or the preserved selection has no kind to match — which is why both
land together.
This is the second half of the report behind 3eb6e5d608: a user comparing the
Design tab with Onshape said "adding constraints seems to be missing"
(OrcaSlicer PR #15238). Constrain was one of the fourteen dead rows, and the
gesture that would have shown it threw the selection away first.
Verified at the machine on behemoth by Tommaso: select a line of a rectangle,
right-click, and the sketch verbs are live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wxMediaCtrl3 and AVVideoDecoder are platform-neutral C++ compiled on
all three platforms, so list them once in the common SLIC3R_GUI_SOURCES
instead of duplicating them in the APPLE and non-APPLE branches. The
else() branch is now empty and drops out entirely.
Co-Authored-By: Claude <noreply@anthropic.com>
gstbambusrc was the GStreamer source element for the old wxMediaCtrl2
Wayland player, its only consumer (deleted in the previous commit).
The new FFmpeg player handles bambu:/// URIs through the Bambu C API
instead. Drop the plugin and the gstreamer-1.0 / gstreamer-base-1.0
REQUIRED pkg-config dependencies that existed solely for it.
Co-Authored-By: Claude <noreply@anthropic.com>
wxMediaCtrl2 was never instantiated on any platform (USE_WX_MEDIA_CTRL_2
is 0 everywhere); wxMediaCtrl3 replaced it. Delete wxMediaCtrl2.cpp/h,
drop them from the Win/Linux source list and the gettext list.txt, and
collapse the preprocessor-dead #if USE_WX_MEDIA_CTRL_2 gate in
MediaPlayCtrl.h.
Co-Authored-By: Claude <noreply@anthropic.com>
The literal --enable-shared was always overridden by ${_link_cmd}
(--enable-static --disable-shared on Apple, --enable-shared elsewhere)
and FFmpeg configure processes these flags in order, last one wins.
Remove it and the stale comment documenting the workaround.
Co-Authored-By: Claude <noreply@anthropic.com>
On a non-English desktop the offer menu came out mixed: "Create / Add material /
Rimuovi / Fillet / chamfer / draft / Repeat / Transform / Reference / Modify",
and under Modify, "Elimina" beside "Constrain sketch".
Nothing was mistranslated. The row names went through a bare wxGetTranslation(),
which searches EVERY loaded catalogue — including wxWidgets' own wxstd. That
catalogue is loaded in the desktop's language whether or not the application has
one, and it happens to contain exactly two of our eight row names:
wxstd it: 'Remove' -> 'Rimuovi', 'Delete' -> 'Elimina'
Create, Add material, Repeat, Transform, Reference and Modify are not wx
vocabulary, so they stayed English. Two words in one language, six in another,
in the same menu — and the same trap is set for every other locale wx ships:
Supprimer, Löschen, Eliminar.
Name the domain: wxGetTranslation(s, SLIC3R_APP_KEY). These strings are now
translated by our own catalogue or not at all, which is consistent either way.
Left deliberately alone: the accelerator still renders as "Canc" rather than
"Del" on an Italian system. That is wx naming the physical key, and on an
Italian keyboard the key really is marked Canc — telling that user to press
"Del" would name a key they do not have.
Verified on the rig with LANG=it_IT: the menu now reads Remove and Delete, and
the submenu shows "Delete Canc" beside "Constrain sketch".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A user evaluating the Design tab against Onshape reported that "adding
constraints seems to be missing" — with nineteen constraint types and a solver
shipped behind it (OrcaSlicer PR #15238, exussum12).
They were not wrong about what they could see. The only ways in were an
icon-only toolbar button whose tooltip you have to hover to read, and an offer
row gated on sketch_mode with a sketch ENTITY selected, filed under "Reference".
Right after finishing a sketch — the moment you want to constrain it — neither
was in front of the user, so a shipped headline feature read as absent.
Add a model-mode row: "Constrain sketch", offered under Modify when a sketch
region is selected, routed through the new btn:constrain verb action.
on_begin_constrain() also gains a fallback to m_sel_sketch_feat. The offer
reaches it from a SkLoop selection, which carries no TREE selection, and the
function read only tree_selection() — so the new row would have answered
"Select a sketch in the tree first" about a sketch the user had visibly
selected. It now adopts the region's owning sketch and syncs the tree to match.
Verified on the rig: draw a rectangle, finish the sketch, click the region,
right-click -> Modify -> "Constrain sketch" enters Constrain mode with
"Pick 1-2 lines, then a constraint" and the Constraints (8) card listing the
sketch's inferred constraints. That path did not exist before.
Does NOT address the other half of the report: there is still no Pierce
constraint, so a sweep profile cannot be tied to its path. Tracked separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mass_properties on an open shell returned volume 96000 with an inertia diagonal
of [-4.2e7, -4.2e7, -6.9e7] for a 60x60x40 four-walled box — negative principal
moments, which no real body can have. BRepGProp::VolumeProperties integrates the
divergence theorem over whatever faces exist; on an open shell that is not a
volume at all, and the old code hid the only obvious tell by taking std::abs()
of the mass. "valid: true" then asserted the number was trustworthy.
This matters because mass_properties is what an agent or a user reaches for to
confirm a cut removed the right material. Silent nonsense there means the check
passes on garbage.
MassProps gains is_solid. For a sheet we compute surface area only — that stays
exact — and report volume 0 with the inertia left zeroed. The MCP verb returns
is_solid plus a note saying volume and inertia are not defined for an open
shell; the GUI's Mass command says "sheet body — N cm² of surface, no volume"
rather than quoting material that is not there.
Verified on the rig: the sheet now returns volume 0.0, surface_area 9600.0
(exactly 4 x 60 x 40), is_solid false. The solid controls are unchanged and
exact — a 60 mm cube reports 216000.0 and 21600.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking a face that already happened to be selected, while a Plane or Axis pick
was armed, read as a repeat pick: the click escalated to "whole body", the
capture was lost, and the card's label stayed "(none)" with nothing on screen to
explain it. On a cube it is easy to hit — the face under the cursor is often the
one already selected from the previous step.
The capture path in on_solid_picked already restores the flag for all three
tools, and reset_plane_refs()/reset_axis_refs() restore it when a pick is
abandoned — both were written as if the arm side disabled escalation. Only
CoordSys actually did (that was snaporca-u0wd). Plane and Axis never had it.
Verified on the rig: Midplane on a 60 mm cube now captures Face A (#5, top) and
Face B (#3, side) on the FIRST click each, and the resulting plane renders as
the 45-degree bisector between them, which is what a midplane of two
perpendicular faces should be. Before this, the first pick escalated to the body
and Face A stayed "(none)".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every method in CadDocument's plane dispatch falls back to offset_angle_plane()
when its references are missing. Picking Tangent and confirming with nothing
selected therefore produced an OFFSET plane, announced as "Plane added — pick it
as a sketch plane". The user asked for one construction and silently received a
different one, with nothing on screen to reveal the substitution.
Validate at the GUI boundary instead: Angle needs an edge, Midplane two faces
(and not the same face twice — that yields a plane coincident with the face,
which is well-defined and useless), Tangent a face, Two-edges two edges. Offset
and Coincident are unchanged: both are meaningful with no reference, since they
fall back to the base plane by design.
on_add_plane() now returns false when it refuses, and confirm_tool() skips
close_tool() in that case — a refusal that also threw away the picks the user
had already made would be worse than the bug.
The kernel keeps fallback_offset(): it must return something. It should just
never be reachable from a user gesture without a warning.
Verified on the Xvfb rig: Tangent with no pick refuses and creates no feature
(it created one before), the card stays open with the type preserved, Midplane
with no faces refuses with its own message, and Offset with no picks still
creates a plane as it always did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wxSpinCtrlDouble takes the mouse wheel whenever the pointer is over it. A card
taller than the panel therefore could not be scrolled past without silently
incrementing whatever field the cursor happened to be over — measured on the
rig: eight notches over the Coord Sys card turned an X hint from 1,00 into 6,00
while the panel did not move at all. The same gesture over an Extrude distance,
a Hole diameter or a mate Offset is a silent model change made by someone who
believed they were navigating, and nothing on screen reports it.
Every spin in this panel comes from one factory, so the guard goes there: an
unfocused spin hands the wheel to its parent, and the scrolled cards panel gets
it. A spin the user has deliberately focused still takes the wheel, which is the
one case where editing is what was meant.
Verified on the rig with an Extrude card: eight notches over an unfocused
Distance leave it at 10,00; clicking into it first and scrolling takes it to
13,00.
A mate between two connectors committed cleanly, recomputed without error, and
moved nothing. apply_mate was reached and computed a translation of exactly
(0, 0, 0).
The connectors were the cause, not the mate. Picking a face stores
coordsys_face and coordsys_body but left the Type combo on its default,
Point (world) — and datum_frame ignores the pick entirely for that type,
resolving the connector to coordsys_point, which is (0,0,0) unless the user
typed otherwise. Two connectors built that way share one frame, so the mate
between them is an identity transform: everything reports success and the
assembly never moves.
Capturing a face or an edge now switches the type to FaceAndDirection. Picking
a face IS the choice of a face-based frame; asking for it twice, with no hint
that the second half is required, is what made every mate a silent no-op.
Verified on the rig, two bodies with a face-based connector each:
before Extrude4 [-71.6, 402.6, 156.5] .. [-8.8, 412.6, 237.3]
after Extrude4 [-31.4, -40.4, -10.0] .. [ 31.4, 40.4, -0.0]
with the mate transform now (40.19, -196.88, 402.56) instead of (0, 0, 0).
That is the first mate in this tree that assembles anything.
Not the kernel: recompute applies mates exactly as preview does, proven by an
A/B harness over all five kinds — the two paths give identical bounding boxes.
1. A consumed loop WITH HOLES was never dropped from the sketch overlay.
sync_sketch_display compares each region's entity list against what a per-loop
extrude stored, but rebuilt the candidate from the region's OWN entities while
the extrude stores the region's entities PLUS every hole's (see
selected_loop_entities). For a plate with one bore that is 4 against 5, so the
match never fired and the extruded rectangle stayed drawn on top of the solid
it had become. Adds region_entity_indices_with_holes, which returns the same
order the extrude uses, and compares against that; a matching region now drops
its holes' entities too. Hole-less regions are unaffected.
This is also the artefact that made a correct plate-with-a-bore read as a
plate with a plug in it during rig testing.
2. The Constraints card drew its header and its first row on top of each other.
The rows and the delete buttons were parented to m_form rather than m_cards,
so they were laid out in the wrong window's coordinate space and started at
the card's top edge. Re-parented; nothing else about the card changed.
3. The mate hover ghost never appeared. The highlight handler asked for a repaint
with request_repaint(), which only queues a Refresh — and a wxMenu popup runs
its own modal loop, so the paint was not serviced until the menu closed, by
which time the ghost had been dropped. Adds DesignCanvas::repaint_now(), which
flushes the paint immediately, mirroring the m_status->Update() the status line
in the same function already needed for the same reason.
Delegated to opencode (DeepSeek V4 Pro) and reviewed by diff. Fix 1 needed an
accessor on DesignSketchTool because region_loops/RegionLoop are private — that
was outside the file list it was given, and it said so rather than working around
it.
Verified on the rig: a rectangle-plus-circle sketch extrudes to a plate with a
bore and NO overlay left on top of it, and the Constraints card shows its header
clear of eight readable rows.
Leaving Sketch clears the "N degrees of freedom" line, which is right — it
describes a sketch's constraint state and means nothing in Feature mode. But
entering CONSTRAIN left it blank too, and Constrain is where the number is the
whole point: the readout is fed only by a live solve, and no solve fires merely
because the mode changed, so the line stayed empty until the user happened to
change something.
The solve callback now caches its last result and the labelling is split into
apply_dof_status(), which set_ui_mode re-applies on entry to Constrain.
Verified on the rig: a rectangle reports "4 degrees of freedom" in Sketch, and
the same line is there after K enters Constrain.
The edge and vertex tolerances were fixed at 8 and 11 px. On a face that is
barely wider than that on screen — a thin plate, or any part once you zoom out —
every point on it lies within the edge budget, so the pick alternated edge and
whole body and the FACE level could never be reached at all. That is not just
awkward: a face pick is what gives a Coord Sys its owning body, and a mate needs
one, so thin parts could not be assembled.
Both tolerances are now capped at a third of the face's shorter on-screen side,
measured from the edge samples the picker already walks. They only ever shrink,
so a face with room keeps the full budget and nothing changes for ordinary
geometry; a narrow one keeps its middle for itself.
Verified on the rig on a 16.7 mm-wide plate at three zoom levels including one
far enough out to make the face a thin sliver: the middle reports "face 5
selected" every time, while points near the rim still take the edge.
A card that asks for a face ("Click a solid FACE in the viewport") could not be
satisfied. Clicking the same sub-element twice deliberately escalates to the
whole body — right for free picking, wrong here: the user clicks the very face
the card is pointing at, the escalation turns it into a whole-body pick, and the
armed capture rejects it and leaves "(none)". Both orders failed, so a
face-based Coord Sys was reachable only by accident of ordering. That mattered
beyond the card: a mate needs a connector with an owning body, and a face or
edge pick is the only thing that sets one.
Adds DesignSketchTool::set_escalate_on_repick, off while the Plane, Axis or
CoordSys card has a pick armed and back on as soon as it is captured or
abandoned. While armed, clicking a face means "this face", which is what the
prompt already says.
Verified on the rig: arm Pick Face, click the face, and it reads "#5" on the
first click. With nothing armed the escalation still alternates whole <-> face
as before.
The mate palette called all five kinds viable on connectors created as
Point(world), which belong to no body. Clicking one built the Mate feature and
the RECOMPUTE then failed with "mate: mate_cs_b has no associated body" — the
refusal arrived one step too late, after the feature was already in the tree,
and the user is told about it by an error line rather than by the palette that
offered the thing.
mate_options now checks the same two conditions the apply path throws on: B must
have a body (B is the connector whose body moves; A is the fixed reference and
needs none), and that body must still resolve. Either way all five kinds go
non-viable with a reason, so the offer and the kernel cannot disagree.
Verified on the rig: with two Point(world) connectors, every row is now dimmed
and reads "connector B is not attached to a body — a mate moves B's body". That
also exercises the dimmed-with-a-reason presentation for the first time, which
until now had nothing to show because every kind was always viable.
Tests: a new [mate] case covering no body, a body that no longer resolves, and
the revival once B is given one. One existing case needed its setup widened
rather than its assertion weakened: "an unrecorded fingerprint does not make a
type non-viable" built two body-less connectors, so it was asserting a side
effect of the old permissiveness instead of the property it is named for. Its
connectors now have a body, leaving the missing fingerprint as the only variable.
Full [CadDocument] suite: 2458 assertions in 185 cases.
Right-clicking a document with two coordinate systems builds the mate palette
exactly as designed: a separator, a disabled "Mate: A -> B" header, then five
rows in fixed order. Hovering a row showed no ghost and left the previous status
message on screen; clicking one created nothing at all. The palette enumerated
perfectly and fired nothing, which put the whole M8 mate epic out of reach from
the UI.
The mate handlers were never invoked. show_offer_menu binds two pairs of
handlers to the SAME wxMenu: the mate pair (by mate_base, at base + 500) and the
generic verb pair. wxWidgets pushes dynamic entries to the FRONT of the handler
list, so the pair bound LAST runs FIRST for every id — and the generic pair was
last. A mate id lands outside the verb table's range, so both generic lambdas
returned early WITHOUT e.Skip(), which wx reads as "handled", and the mate
handlers behind them never saw the event.
Binds the two generic handlers to the verb id range [base, base + 499] so each
half only sees its own ids regardless of bind order.
Verified on the rig with a purpose-built assembly (two bodies 60 mm apart, a
Coord Sys on each): before, clicking Fastened produced no feature and no status
change; after, it produces a Mate feature. The verb rows keep working — the same
run created both coordinate systems through Reference > Coord Sys.
Note the mate then fails its recompute with "mate: mate_cs_b has no associated
body", because a Point(world) coordinate system belongs to no body while the
palette still advertises all five kinds as viable. That is a separate defect and
is filed; this commit is about the palette being reachable at all.
The test executables that link libslic3r_gui (which links PkgConfig::LIBAV)
have a load-time dependency on the deps-built FFmpeg shared libraries. The CI
unit-test runner only receives the tests artifact, so those libraries were
unresolvable there (Ubuntu 24.04 ships libavcodec.so.60, not .61). Copy the
libraries next to each affected test executable and give it an $ORIGIN rpath,
mirroring the Windows branch that copies DLLs next to every test executable.
orcaslicer_copy_sos now places the copies in the per-config output directory
for multi-config generators, like orcaslicer_copy_dlls does.
Co-Authored-By: Claude <noreply@anthropic.com>
Reopening a saved CAD design ended on a modal "The file does not contain any
geometry data." warning. A CAD project legitimately carries no mesh — the model
lives in the feature tree (Metadata/SnapOrca_cad.bin) until Commit to Plate — so
the warning is false for one, and it is the last thing the user sees after
opening a design they spent an hour on. It reads as "your work is gone" at the
exact moment the recipe HAS just loaded and the Design tab is about to rehydrate
it, and the main window sits disabled behind the dialog until it is dismissed.
Counts a non-empty model.cad_recipe as geometry.
Verified on the rig: save without Commit to Plate, reopen, and the Design tab
comes up with Sketch1 -> Extrude2 -> Body 1 editable, with no dialog at all.
A rectangle with a circle inside it, drawn in ONE sketch, could not be extruded
to a plate with a bore from the GUI. Five defects were in the way. Each was
found by driving the app on a headless rig and measuring the result — the code
reads correctly at every one of these points, which is why they survived.
1. Wire orientation (kernel). SketchEngine::wires_to_face added every hole as
wires[i].Reversed(), which is only right when the sketch happens to wind both
loops the same way. A circle drawn clockwise inside a counter-clockwise
rectangle came out matching the outer boundary, OCCT swept it as a SECOND
contour, and the prism was the plate with its bore filled and the disc's
volume counted twice. Measured: bbox 67.17 x 219.67 x 10 with volume
152088 mm3 against a solid box of 147542 — a body larger than its own
bounding box, which is the signature. Holes are now added as-is and
ShapeFix_Face::FixOrientation() classifies them; that is winding-independent
and is the idiom make_extrude_regions already used for imported glyphs, which
is why holed TEXT always extruded correctly while a holed SKETCH never did.
After the fix: 142996 mm3, implied bore radius 12.03 mm against the circle
drawn.
2. The live-sketch click threw the picked region away. region_at() served only
as a yes/no gate and on_face_selected() carried no argument, so Extrude fell
back to whichever loop the resolver found first — clicking the material of a
plate-with-a-hole extruded the disc. The region is now carried through, and
DesignPanel also hands it to the tool with set_loop_pick(), AFTER open_tool()
because that re-derives selection state, since extrude_uses_loop() reads
selected_loop_entities() and that lives on the tool.
3. region_at() had no hole awareness and no innermost preference: it returned
the first polygon containing the point. It now skips a region when the point
lies inside one of that region's holes, and picks the smallest containing
loop, so a click in the bore selects the disc and a click on the material
selects the plate.
4. Startup segfault. DesignCanvas::request_repaint probed the GL backend via
OpenGLManager::get_gl_info().get_renderer() before the canvas had initialised
GL — glGetString with no context current and, before init_opengl(), no loaded
function pointers. Anything that asked for a repaint while the panel was
still being built landed there, with no window and nothing in the log. It now
bails at the top on !is_initialized() and asks for a Refresh instead. Note
the crash was in the PROBE, not in render(), which already guards itself.
5. A holed sketch on a plane whose normal points -Z came out as the full box PLUS
a disc — 220274 mm3 where 163726 was due (192000 + 28274). wires_to_face took
a SketchPlane parameter it never used and let OCCT infer a surface from the
outer wire; when the inferred normal disagreed with the sketch's, the hole
classification produced no hole. Every face is now built on the sketch's own
gp_Pln.
Also in this change, from the same rig session:
- A right-click that only clears the sketch selection no longer reports itself
as consumed, so it stops suppressing the offer menu. With any geometry in a
live sketch there was no menu route left to add a second entity.
- Escape no longer discards a live sketch that holds drawn geometry; it says so
and keeps the work (live_sketch_has_work()).
- The holed-region fill is an even-odd scanline instead of a keyhole bridge, so
no corridor triangle leaks from the bore to the nearest corner.
- Cyan is reserved for the selection: an unselected region no longer wears a
shade one step off the selected one.
- The origin planes follow the mode, so pressing Sketch on a document that
already has a body offers them again instead of naming a plane you cannot see.
Tests: three [holes] cases over add_extrude_entities asserting the plate-with-bore
volume, solid and face counts on both a +Z and a -Z sketch plane, and the
by-name refusal of two disjoint regions. Full [CadDocument] suite green.
Delete a sketch on a document that still has a body and you could not start a new
one. Pressing Sketch said "click a face or a reference plane in the viewport" —
while update_reference_planes had already called clear_base_pick, because a body
existed. The instruction named something that was no longer there, and short of
finding a face to click there was no way back into sketching at all.
The planes are now offered when there is no solid yet OR while the UI is in
Sketch mode, and set_ui_mode refreshes them so they appear the moment you press
Sketch rather than at the next tree rebuild — which is not an event that pressing
Sketch causes.
Deliberately not always-on. m_dbp_active both RENDERS and picks, so leaving it
set would float three translucent planes over every finished model. Tying them to
the mode shows them exactly when they are the thing being chosen and takes them
away again on Finish.
Safe against stealing clicks: a base-plane pick is the last resort in on_mouse,
firing only on a click that hit no geometry, so solids and committed sketches
still win where they overlap.
Found on the rig by Tommaso: "if i remove a sketch, i cannot create sketches
anymore".
A rectangle with a circle inside it extruded to a plain box. Tommaso reported it
exactly right on the first attempt — "no intersection selectable, hence no plate
with hole" — and it was a causal chain, not a guess.
Two things were wrong and they compounded.
region_loops() returned N independent filled polygons with no notion of nesting,
so the only selectable things were the rectangle alone and the circle alone. The
region a user actually wants — the bounded area WITH its hole — did not exist to
be pointed at. Worse, the first polygon containing the click won, so clicking
inside the circle selected the rectangle.
And extrude_uses_loop() hands selected_loop_entities() to add_extrude_entities,
which copied only that one loop's entities into the feature. So the circle never
reached the kernel, build_sketch_face saw a single loop, and the multi-loop path
added in 5c4ced91e7 never ran. Proven from his saved project: Sketch1 held 5
entities, Extrude2 held 4, and the committed mesh was 8 vertices — a box of
260.40 x 220.91 x 10.00. Eight vertices cannot describe a bore.
A RegionLoop now carries the loops nested inside it. Containment is decided by
testing one vertex, which is sufficient because loops in a well-formed sketch do
not cross, and each loop is assigned to the SMALLEST loop containing it so a hole
belongs to the region that actually bounds it. Picking respects holes: a click in
the plate selects the plate, a click in the bore selects the disc. The selection
hands over the region's own entities plus its holes', which is what finally
reaches the kernel. The highlight lights the holes with their region, because it
has to show what will be extruded.
The status line said "Loop selected"; it now says "Region selected". What is
selected is a bounded area that may contain holes, not a single closed curve —
the old wording described the old, broken behaviour.
snaporca-txp8, and it is what makes snaporca-88v reachable from the GUI at all:
the kernel could build the holed face all along (verified on his own recipe:
2 closed loops, wires_to_face OK, area 74812.119 mm2), but nothing could ask it to.
Reviewed and compiled (RC=0). NOT exercised — the rig check is the point.
The AppImage dependency closure resolves each bundled ELF's DT_NEEDED
entries with plain ldd, which cannot resolve the deps-built FFmpeg stack
(libavcodec/libavutil/libswscale) once it is copied into the bundle:
those libs are not installed in any standard loader path and carry no
RUNPATH of their own, so ldd reports the siblings as missing and the
build aborts. Extend the loader path with the bundle directory plus the
source directories of already-bundled files (mirroring
scripts/check_appimage_libs.sh), and key the dedup set on the bundled
file path instead of the source path so dependencies resolved from the
bundle directory are not copied onto themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
Modelling in the Design tab and pressing Ctrl+S saved a project with no feature
history at all, and the app reported success. Found on the rig: a project saved
after drawing a rectangle and a circle contained twelve archive entries, none of
them Metadata/SnapOrca_cad.bin, and a 3dmodel.model with zero vertices.
plater->model().cad_recipe was assigned in exactly one place — on_commit(),
immediately after load_mesh_object. The 3MF exporter was never at fault: it
faithfully wrote whatever the Model held, and on a save that had not gone through
Commit to Plate that string had never been set. The recipe reached the Model only
as a side effect of a different user action.
It now tracks the document instead. sync_recipe_to_model() is called after a
successful recompute, after tree edits (deletes, reorders and suppressions bypass
recompute_guarded), and from on_commit, which delegates rather than repeating the
rule. Only on success — a failed recompute leaves the document mid-edit, and
persisting that would save a model the user never had. An empty document still
clears it, so a non-CAD project carries no stale recipe.
Doing it here rather than in the save path is deliberate: Ctrl+S, Save As,
autosave and crash recovery all read model.cad_recipe, so keeping it current
after each change makes every one of them correct at once, instead of teaching
each save path to ask the Design tab. Commit to Plate means "send this to the
slicer" — making saving depend on it was the bug, not the cure.
Cost is one serialization per recompute, tens of KB against an OCCT rebuild that
has just run.
Why nothing caught it: the kernel round-trip tests serialize a CadDocument
directly, and the 3MF tests exercise the exporter with a recipe already present.
Neither can observe that the GUI never populates it, and every save in testing
happened to follow a Commit to Plate.
snaporca-vjk5. Reviewed and compiled (RC=0); persistence NOT yet confirmed on the
rig — that check is the reason the issue stays open.
wxMediaCtrl_OnSize referenced wxMediaCtrl2's private m_gtk_video_window,
which does not compile on Linux/GTK. Move the resizing into
wxMediaCtrl2::DoSetSize where the member is in scope.
This script builds only libslic3r_tests, which links libslic3r and no GUI code —
but cmake still processed the whole if(SLIC3R_GUI) block and every find_package
inside it, so the kernel suite silently depended on the GUI's dependency set.
That came due the moment upstream added wxInspector as a REQUIRED find_package:
the orcacad-deps image predates it, so configure died pointing at
src/CMakeLists.txt:92 with nothing about the kernel having changed. Turning the
block off is not a workaround for that one dependency — it is the suite finally
declaring what it actually needs, so the next GUI-side dependency added upstream
cannot break it either.
Surfaced by taking SoftFever's merge of main into the PR branch.
He merged upstream main into the PR branch himself on 2026-08-13. Taking it into
the local branch rather than force-pushing over it: the fork copy is what PR
#15238 shows, and discarding a maintainer's merge to make my own push
fast-forward would be both rude and a loss of 130 upstream commits.
Brings the branch far closer to main than the 2026-07-24 merge-base the PR body
describes, which is most of what snaporca-36u9 was filed for.
Every version bump so far has permanently orphaned every project saved before
it. deserialize_recipe refused anything that was not exactly the current
version, and with no migration path v2 and v3 projects are unopenable today —
the 3MF still carries the mesh, so the user gets a frozen solid and no feature
history, which is the whole point of the subsystem silently absent.
The cause was the shape of the data, not the gate. save/load is one flat
symmetric list of ~90 fields with no framing, so a reader has no way to know
where a feature ends unless it agrees on every field.
Each feature is now written as its own cereal stream behind a length prefix, and
the same few lines handle both directions of mismatch. Older file, newer build:
the sub-stream ends early, the read throws, and the fields already assigned are
kept while the rest default — cereal assigns sequentially, so a mid-list throw
leaves the earlier fields set, and that is what makes this work. Newer file,
older build: the sub-stream holds more bytes than the reader knows; it reads what
it knows and stops, and the outer stream is untouched because the length prefix
was consumed in full. A field a project predates is not a corrupt project, so
neither case is an error.
v4 keeps its own pre-framing flat path and opens exactly as before —
cad_recipe_v4.bin is untouched and now serves as the witness for that. v2 and v3
stay refused, by name: their field lists no longer exist in this code. This fixes
the future, not the past, and the comment says so rather than implying otherwise.
From here a new field only needs appending to save/load — no bump, no orphaned
projects. That removes the cost that had blocked snaporca-44m and snaporca-dgv.
The helix round-trip test was reading the blob back flat, reaching into the
format instead of through it; framing necessarily breaks that, so it now goes
through deserialize_recipe, which is a stronger assertion than it made before.
Every field check it carried is unchanged.
Tests: four new [CadDocument][recipe] cases, including the one the change exists
for — a deliberately truncated feature blob must LOAD, keeping what it could read.
Suite 177 -> 181 cases, 2366 -> 2411 assertions.
snaporca-2txy.
deserialize_recipe distinguishes three cases that matter very differently to the
person reading the message — saved by a NEWER build, saved by an OLDER one, or
genuinely unreadable — and names the version in each. load_recipe threw all of
that away and printed one generic sentence, so the user could not tell "update
SnapOrca" from "your file is damaged", and had no way to find out.
Same error-loss class as the 31 McpControl sites fixed in 1de72de9ed: the message
existed, it was simply not passed on. The generic sentence stays as the fallback
for the case where the kernel really has nothing to say.
This does not make old projects loadable — that is snaporca-2txy, which the audit
behind this change opened. It only stops the reason being withheld.
snaporca-2txy (partial). Reviewed and compiled (RC=0), not exercised.
entities_to_wire handled exactly two shapes of sketch: one lone Circle/Ellipse, or
any number of Line/Arc/EllipseArc/BSpline pushed into a single MakeWire. Everything
else fell off the end as a null wire, so a circle drawn inside a rectangle — the
most ordinary thing in this whole program — refused with "not supported yet". Two
separate closed polygons were quietly worse: both went into one MakeWire, which
does not mean "two loops" to OCCT.
entities_to_wires now returns one wire per loop. A Circle or Ellipse is a loop on
its own; chain entities are grouped by shared endpoints (union-find, 1e-6 in sketch
coordinates), and an open chain still comes back as a wire because a sweep path is
legitimately open. It is all-or-nothing: one loop that fails to build poisons the
whole result, because a partial profile would extrude a shape the user did not draw
— the failure 2e6a8f9e91 was written to stop.
entities_to_wire survives as a two-line wrapper returning the single wire when
there is exactly one loop and a null wire otherwise, so all nine of its call sites
keep their exact contract and Revolve/Sweep/Loft/Surface* are untouched. What a
holed profile means for each of those is a separate question.
wires_to_face takes the largest-area loop as the outer boundary and adds the rest
reversed, which is how OCCT is told a wire is a hole. Containment is CHECKED with
BRepClass_FaceClassifier, not assumed: a loop outside the largest one is a second
island, and one sketch producing several solids is a much bigger feature, so it is
refused by name ("two disjoint regions") rather than guessed at.
Only the Extrude case consumes the new face. Tapered extrudes of a holed profile
are refused — offsetting inner loops has to go the opposite way — and the guard
counts wires on the face already built rather than rebuilding every wire to ask how
many there are, which is also the more honest test: what matters is the profile
being extruded.
Tests: six new [CadDocument][sketchwire] cases, proved by VOLUME rather than by not
throwing — plate-with-hole, two holes, and two regression guards that a lone circle
and a lone polygon extrude exactly as before. Suite 177 cases / 2366 assertions.
No serialized field, recipe version untouched, golden fixtures unchanged.
snaporca-88v.
Third and last piece of the selection model. The other two turned out to be
built already — the rubber band is pick_bodies_in_rectangle and vertex picking
is SolidSel::Vertex with its camera-facing square, both live — so this closes
what the issue actually still described.
Vertex beats edge beats face is a rule the user cannot see until after they have
committed to a click. Showing the outcome under the pointer is what makes the
precedence learnable at all, and is the charter's L5 read honestly: one click,
one visible change means the change has to be predictable BEFORE the click, not
only explicable after it.
The resolution is now one function, resolve_solid_pick, const and writing only
into its out-parameter. The click applies it and then runs its escalation
unchanged; the hover applies nothing. Split this way the promise cannot drift
from the act — a second implementation of "what is under the cursor" would
eventually disagree with the first, and the disagreement would look like a
picking bug rather than a duplication one.
Rendering is likewise one function called twice. The pre-highlight draws first
so the committed selection paints over it, and is suppressed entirely when the
two are the same thing: two coats of the same colour reads as a rendering fault,
and a promise about a click that would change nothing is not worth making. It is
desaturated toward white rather than given its own hue — a distinct colour would
read as a distinct KIND of selection, when it is the same selection one moment
earlier.
Two things that would have been silent bugs. The edge ribbon and the vertex
square render with GL_BLEND off, so an alpha below 1 there is ignored; those two
are quietened by a muted rgb and only the blended face fill takes the alpha
multiplier. And the pre-highlight is cleared in clear_solid_selection, because it
names a face by an index into a shape a recompute has just rebuilt — left behind,
it would keep glowing on whatever now sits at that index, a real entity but not
the one meant.
Hover runs on plain motion only, with no button down and no band running: during
a drag the pointer is doing something else and a promise about clicking would be
a lie. It returns false so the event still reaches the camera — it asks for a
repaint, it does not consume the gesture.
snaporca-9xw. Reviewed and compiled (RC=0), not exercised.
Tommaso reported the array controls as missing. They were not — Shift+N opens a
Pattern card with every control correct — but the report was fair. With no body
the button accepts the click, opens nothing, and writes its refusal somewhere
other than where the click happened. From the user's seat that is
indistinguishable from a dead button, and the icon is one unlabelled glyph among
fourteen, which is how I mis-clicked it into Section view while reproducing this.
A control that cannot act should look like it cannot act, before it is pressed.
The three FEATURE buttons carrying a body-count guard — Pattern and Cut at one
body, Boolean at two — are now greyed below their threshold with a tooltip
naming what is missing.
Only those three. The same guard shape also appears on rows INSIDE the flyouts,
and those stay live: a drawer holds sketch-only entries too, so disabling the
drawer would hide tools that are perfectly usable. The keyboard shortcuts keep
running the guarded action rather than being gated — a key press has no
greyed-out state to see, so the sentence is the only feedback there is.
Re-evaluated in feed_bodies(), before its viewport early-return since this is
about the toolbar and not the canvas, and once after the toolbar is built: an
empty document is the state the bug was reported in and feed_bodies has not run
yet on a fresh tab.
snaporca-o9j. Reviewed and compiled (RC=0), not exercised.
Two independent leftovers, both in the same status area.
snaporca-752: the "N degrees of freedom" line described a sketch's constraint
state and stayed on screen after Confirm, Cancel and the Escape downgrade, in
Feature mode where it means nothing — visible in every Feature-mode screenshot of
the 2026-07-27 sweep. Cleared in set_ui_mode rather than at those three exits,
because that is the one place all of them pass through and a fourth exit added
later would otherwise reintroduce it. Constrain mode keeps the readout: that is
where the number is the whole point.
snaporca-8cc: moving the status out of the panel and into the viewport HUD
removed the clipping, but not the underlying problem. The chip is a top-level
popup that Fit()s to its text, so a long sentence grew past the right edge of the
canvas and hung over the window instead of being cut off inside it — the same
silent length limit wearing a different hat. The label now wraps to the room
actually available (canvas width minus the view-cube inset), which is what makes
the earlier promise that "a sentence can be a sentence" true at 1366 as well as
at 1920.
SetLabel + Wrap + Fit are now one function called from both the text change and
the placement. Wrap() rewrites the label it is handed, so it has to follow a
fresh SetLabel every time, and the placement path runs on resize — a chip wrapped
for the old width either overhangs a narrowed canvas or wastes a widened one.
The left inset is one constant now because the wrap width and the anchor have to
agree, or the chip wraps to a width it is not then given.
snaporca-752, snaporca-8cc. Reviewed and compiled (RC=0), not exercised.
The .pot, the Italian .po and list.txt are build product: 27,314 of the added
lines in this branch were regenerated catalogues rather than code, and a reviewer
running git diff --shortstat met that number before anything else. Restored to
the merge-base so their diff is zero; they regenerate from source with
scripts/run_gettext.sh whenever the maintainers want them refreshed.
The Romanian catalogue goes with them, for a different reason: it is a complete
new translation and deserves its own PR rather than riding along inside a CAD
feature, where nobody qualified to review it would think to look.
Nothing here changes what the Design tab does. The strings are still marked for
translation in the sources; only the generated catalogues are out.
m_hole_on_face and m_thread_on_face are cleared only by their tool's flyout and by
their plane combobox, so after any on-face hole or thread the flag stays true for
the rest of the session. load_feature_into_dialog restored the stored plane into
the dropdown but never touched the latch, so re-editing from the feature tree
ignored the plane it had just restored: hole_plane() returned the still-latched
face plane, which may belong to a different face, a different body, or a body
since rebuilt. Silent until snaporca-200 added the "On face" row, which then read
as a confidently wrong answer rather than as nothing.
The latch is now rebuilt from the stored feature, which is the only source that
describes THIS hole. Not from the dropdown row: index_from_plane snaps an
arbitrary face plane to the nearest XY/XZ/YZ, so driving the re-edit from the row
would MOVE a hole drilled on a slanted or offset face — that was the reason the
other candidate fix was rejected.
is_base_plane() decides which of the two a stored plane is. It compares the origin
as well as the axes (a plane parallel to XY but 12 mm up snaps to row 0 and would
come back at z=0), and adds modeling_origin before comparing, because hole_plane()
and thread_plane() add it to the dropdown plane before the feature stores it — a
document with a shifted origin would otherwise mistake every dropdown hole for a
face pick. Vector norms, not isApprox, which is relative to magnitude and useless
against the zero origin.
The face's (u,v) extent is not serialized, so m_hole_has_bounds is cleared: the
gizmo's footprint clamp goes unbounded, which is honest, where another face's
bounds are not. The label says which body the face belongs to instead of a face
number the feature does not carry; "(none — uses Hole plane)" is the one thing
that is definitely false there.
snaporca-uif9. Reviewed and compiled (RC=0), not exercised.
refresh_preview() listed Tool::Mate among the features that produce no solid and
cleared the ghost, with a comment saying a mate has no 3D ghost. The kernel never
agreed: preview() routes a Mate candidate through apply_mate on a throwaway copy
of the bodies, and build_candidate already filled the mate fields. That one early
return was the whole of epic gap G3.
A mate makes no NEW geometry but it MOVES a body, and the moved assembly is the
ghost worth showing. Both the Mate card and the offer's mate palette now show it:
hovering a palette row previews that kind, leaving the row drops it, and choosing
one commits. Nothing is written to the document until the click.
The committed bodies are hidden while the ghost is up — it is the whole assembly
in its post-mate pose, not an added lump, so leaving them visible would draw the
mated body twice and z-fight every other body against its own copy. Same reason
Dressup and Draft hide them.
Cleanup is after PopupMenu rather than on a close event: PopupMenu is modal, so by
then the menu is gone and any command it raised has run. A flag distinguishes a
ghost this menu put up from a preview that was already on screen.
snaporca-b4sp. Reviewed and compiled (RC=0), not exercised.
src/slic3r/GUI/wxMediaCtrl3.cpp:181:23: error: ‘info’ was not declared in this scope
181 | BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
| ^~~~
src/slic3r/GUI/wxMediaCtrl3.cpp:181:5: error: ‘BOOST_LOG_TRIVIAL’ was not declared in this scope
181 | BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
| ^~~~~~~~~~~~~~~~~
(cherry picked from commit c5c41e20ca2fc7f3b53a4c769961f73df6992008)
src/slic3r/GUI/wxMediaCtrl3.cpp: In member function ‘void wxMediaCtrl3::paintEvent(wxPaintEvent&)’:
src/slic3r/GUI/wxMediaCtrl3.cpp:121:5: error: ‘wxPaintDC’ was not declared in this scope; did you mean ‘wxPoint’?
121 | wxPaintDC dc(this);
| ^~~~~~~~~
| wxPoint
(cherry picked from commit 9ab5009235d212699f91e01d7f930f92849ed1e3)
src/slic3r/GUI/wxMediaCtrl2.cpp: In lambda function:
src/slic3r/GUI/wxMediaCtrl2.cpp:170:13: error: ‘wxMessageBox’ was not declared in this scope; did you mean ‘wxInfoMessageBox’?
170 | wxMessageBox(_L("Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Bambu Studio?)"), _L("Error"), wxOK);
| ^~~~~~~~~~~~
| wxInfoMessageBox
src/slic3r/GUI/wxMediaCtrl2.cpp: In member function ‘void wxMediaCtrl2::Load(wxURI)’:
src/slic3r/GUI/wxMediaCtrl2.cpp:179:5: error: ‘wxLog’ has not been declared
179 | wxLog::EnableLogging(false);
| ^~~~~
(cherry picked from commit 73908d38d8b1f7c8dcae92d55711bc08cbfff23c)
In file included from src/slic3r/GUI/AVVideoDecoder.cpp:1:
src/slic3r/GUI/AVVideoDecoder.hpp:28:20: error: ‘wxImage’ has not been declared
28 | bool toWxImage(wxImage &image, wxSize const &size);
| ^~~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:28:36: error: ‘wxSize’ has not been declared
28 | bool toWxImage(wxImage &image, wxSize const &size);
| ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:38:10: error: ‘vector’ in namespace ‘std’ does not name a template type
38 | std::vector<uint8_t> bits_;
| ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:9:1: note: ‘std::vector’ is defined in header ‘<vector>’; did you forget to ‘#include <vector>’?
8 | #include <libswscale/swscale.h>
+++ |+#include <vector>
9 | }
src/slic3r/GUI/AVVideoDecoder.cpp:145:89: error: invalid use of incomplete type ‘class wxBitmap’
145 | bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
| ^
(cherry picked from commit 781ce14e061366da64fdc2d0d592fa35ee57e67e)
src/slic3r/GUI/wxMediaCtrl3.h:80:10: error: ‘condition_variable’ in namespace ‘std’ does not name a type
80 | std::condition_variable m_cond;
| ^~~~~~~~~~~~~~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::condition_variable’ is defined in header ‘<condition_variable>’; did you forget to ‘#include <condition_variable>’?
26 | #include "Printer/BambuTunnel.h"
+++ |+#include <condition_variable>
27 |
src/slic3r/GUI/wxMediaCtrl3.h:81:10: error: ‘thread’ in namespace ‘std’ does not name a type
81 | std::thread m_thread;
| ^~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::thread’ is defined in header ‘<thread>’; did you forget to ‘#include <thread>’?
26 | #include "Printer/BambuTunnel.h"
+++ |+#include <thread>
27 |
In file included from src/slic3r/GUI/MediaPlayCtrl.h:17,
from src/slic3r/GUI/MediaPlayCtrl.cpp:1:
src/slic3r/GUI/wxMediaCtrl3.h:77:13: error: field ‘m_frame’ has incomplete type ‘wxImage’
77 | wxImage m_frame;
| ^~~~~~~
(cherry picked from commit 727a73333bd67acf5ff2b1c51ff284c2bacdb413)
snaporca-lukg part B. The issue describes building a contextual viewport palette
with a stable icon set, non-viable options dimmed and explained rather than
hidden, and edge-aware placement. show_offer_menu() already does all three — its
dead-row branch appends a disabled row with " — " and a reason, and wxMenu
places itself against the screen edge. So this is not a new widget. It is one
section added to that menu, fed by mate_options().
The header row names the pair: "Mate: A → B". That is epic gap G4 — the mate card
is abstract dropdowns and never says which body moves. B is the connector on the
body that MOVES, so B is the arrow's destination; the parameter order invites the
opposite guess, which is why it is commented at the point of use.
Five rows in one loop over the kernel's result, never reordered and never
filtered. The palette addresses rows by position, so a shorter list would move
every row below it — which is the whole argument for dimming instead of hiding.
The pair comes from the Mate card's combos when that card is open, so the offer
and the card cannot disagree about what they are acting on; otherwise the first
two enabled connectors, which is defensible only because the header names them.
An offer acting on an unnamed pair would be worse than no offer.
REVIEW CATCH: the five type names arrived as an array indexed by kind, read as
_L(table[i]). That compiles and is silently untranslatable — _L is a gettext
macro and the extractor scans SOURCE for literals, so five strings would have
shipped that are never in the catalogue. These names appear nowhere else in the
tree, so that would have been their only occurrence. Now a switch of literal
_L() calls.
G3 INVESTIGATED, NOT BUILT, as specified. preview() DOES handle a Mate candidate:
it copies the committed bodies to a temporary and routes the candidate through
apply_mate on that copy, committing nothing, and build_candidate already fills
the mate fields. The only blocker to a hover preview is refresh_preview()'s
Tool::Mate early return, which clears the preview on the belief that a mate has
no ghost. Nothing in the kernel refuses it. Filed rather than built.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-lukg.
snaporca-lukg wants a palette offering all five mate types with the non-viable
ones DIMMED AND EXPLAINED rather than hidden — its reasoning being that a menu
changing shape between invocations destroys the motor memory experts rely on.
That needs an answer this document could not give. This is that answer, and
nothing else: mate_options(cs_a, cs_b) returns five MateOption{kind, viable,
reason}, always five, always in kind order, never filtered.
The geometry test rides on the fingerprint added for snaporca-kqih, which is why
it costs no new serialized field: coordsys_face_kind already records the surface
type. Revolute and Cylindrical need a cylindrical face at both ends because they
need an axis to turn about; Planar needs flat faces; Fastened and Slider
constrain frames rather than surfaces, so no geometry test applies to them.
UNKNOWN IS PERMISSIVE. A fingerprint of -1 means PointWorld or a connector that
has not resolved yet, and it does NOT make a type non-viable. Refusing on missing
information is the false-alarm behaviour that gets a whole feature ignored — the
same reasoning already recorded on kqih for the drift warning, applied again
because it is the same trade.
Reasons name WHICH connector is the problem when only one is. "needs a
cylindrical face at both ends" tells the user what the rule is; "connector A is
on a flat face" tells them where to look, and the second half is the one that
saves the time.
The stability contract has its own test, asserting five entries in kind order
even for a completely invalid pair. That matters more than any individual
verdict: the palette addresses rows by position, so a shorter list would move
every row below it.
Golden fixture unchanged — this is a pure query. Suite 167 -> 171 cases,
2284 -> 2348 assertions, green. snaporca-lukg part A; the palette is part B.
snaporca-rgbj measured the damage: four chamfers on a box remove
0.400/0.397/0.397/0.395 mm3 when each id is re-read, and
0.400/0.008/0.397/0.280 when the four ids are captured up front. The kernel is
right in both runs — the second one asks for the wrong edges. Neither errors,
because a stale id still resolves to a real edge, just not the one that was
measured.
That makes it an API problem rather than a script bug. Reading the scene once and
then issuing several operations is the natural way to drive a socket, it is what
every agent will write, and it produced silently wrong geometry with nothing
anywhere reporting it.
CadDocument::topo_generation is bumped where the bodies are replaced — the single
line in recompute() where the face and edge maps actually change, so a feature
type added later cannot forget to bump it, which a per-mutator counter would
invite. describe_scene and query_topology return it. A caller may pass it back as
"generation" on any call, and a mismatch is refused with a message that says what
to do about it.
Two deliberate choices:
OPTIONAL, not mandatory. Every existing script keeps working unchanged; passing
the generation is what buys the guarantee. Making it required would break every
caller to fix a mistake only some of them make.
CHECKED AT THE DISPATCHER, not in each handler. One site covers fillet, chamfer,
shell, draft, coordsys, thicken, cut, project, delete_face and everything added
after them. A per-handler check is a list that goes stale the first time someone
adds a method in a hurry.
Not serialized: an id means something only within the run that produced it, so
persisting the counter would promise a stability the ids themselves do not have.
No recipe version change.
describe_tools now carries an id_lifetime note, because the guard only helps a
caller who knows to ask for it.
Kernel suite 167 cases / 2277 assertions green; libslic3r_gui builds. The guard
itself is NOT exercised — it needs the socket, so it is on snaporca-bdco.
snaporca-o1l2.
kqih option (c). A FaceAndDirection connector stores a global face index, and an
upstream edit can renumber faces so the index silently names a different one. The
DANGLING case already threw; this is the in-range-but-wrong case, which nothing
detected.
Fingerprint the face on first resolve, compare afterwards, and report a mismatch
into mate_conflicts — the channel that already marks the tree row — never as an
error. A drift warning must not abort the recompute, because the alternative
makes a legitimate Draft on a mated face fatal.
WHAT THE FINGERPRINT IS, AND WHAT IT IS NOT. Surface type plus edge count. Not
centroid or area: legitimate parametric edits move and resize faces, which is the
entire point of the model, so either would fire on every dimension change. Not
the normal, which is the tempting one — Draft deliberately tilts a face and
Transform reorients a body, both legitimate. Type and edge count survive rigid
motion, tilting and resizing, and catch the case that actually happens: a planar
index sliding onto a fillet's cylindrical face after a dress-up inserts faces.
The accepted cost is that a slide between two planar 4-edge faces is invisible. A
partial detector that never cries wolf beats a total one that does, because a
false alarm on a valid connector teaches people to ignore the warning.
Connectors with no fingerprint record one on first recompute, so old recipes
self-heal and both writers (DesignPanel, McpControl) get it without changing.
THE VERSION BUMP IS THE IMPORTANT HALF. The task was specified with "do not
change the recipe version" — that was wrong, and the rule is written in the
header three lines above the constant: bump whenever save/load gains a field.
deserialize_recipe() gates on v == VERSION and then reads a FLAT symmetric field
list. A v3 blob under a v3 build that has grown two fields passes the gate and
reads two ints past the end of every connector, into the next feature's bytes.
That is silent corruption of a saved project, which is worse than any load error.
Now v4, and v3 gets the existing clean refusal.
cad_recipe_v3.bin is KEPT, unregenerated, with a test asserting it is refused and
that nothing half-read is left behind. It is the only artefact that can prove the
gate works, because it was written by an older build — regenerating it with
today's code would destroy the evidence, which the test says in as many words.
Suite 163 -> 167 cases, 2248 -> 2277 assertions, green. Fixture v4 34928 bytes.
snaporca-kqih.
Two tests that separate a hypothesis nobody had tested. The socket showed four
chamfers on a filleted rim removing 29.6 / 20.0 / 10.3 / 7.5 mm3, falling
steadily. That could be the chamfer maths degenerating on a filleted rim, or it
could be how the driver captured its edge ids. Those have completely different
fixes, so the first job was to find out which.
dressup_edge is a global index into TopExp::MapShapes(shape, TopAbs_EDGE),
resolved against the body AS IT STANDS at that feature's position, and every
dress-up rewrites that map. So the two usage patterns are:
ids re-read after each chamfer: 0.400, 0.397, 0.397, 0.395 mm3 (max/min 1.01)
four ids captured up-front: 0.400, 0.008, 0.397, 0.280 mm3 (max/min ~48)
The kernel chamfers uniformly when handed a fresh id. It degrades only when
handed ids snapshot against an earlier shape — and the second chamfer's stale id
landed on a nearly-consumed edge and cut two percent of what was asked. That is
the accumulating-drift signature the socket showed.
Conclusion: driver artefact. apply_chamfer and OCCT are not at fault.
The part that makes this worth a test rather than a note: IT DOES NOT THROW.
ok=1, error empty. A stale id still resolves to a valid edge — just the wrong
one — so nothing anywhere reports it. Silent wrong geometry, which is the class
this project does not tolerate, reachable by any caller that reads the scene once
and then issues several dress-ups.
Test 2 asserts the non-uniformity as CURRENT BEHAVIOUR and says so in the code:
it documents a defect, it does not bless one. When the driver contract is fixed
it should be rewritten, not deleted.
Suite 161 -> 163 cases, 2217 -> 2248 assertions, green. Tests only, no
production code. snaporca-rgbj.
These were the last tools from the charter audit with no handle at all, and the
issue filed against them offered three options, all of which changed the tool.
Reading on_add_surface_offset() dissolved the question instead.
The premise was that a distance handle needs a frame, a sheet body has no single
normal, and therefore the tool must start demanding a face. But the face was
never needed for the OPERATION — only for the ARROW. Both tools still offset or
thicken the entire sheet named in the combo. The picked face only says where to
stand the handle.
So the arrow appears whenever a face of that sheet is under selection, and its
absence costs nothing: the card alone works exactly as before. Purely additive —
no existing flow changes, and there is no new precondition for the user to learn.
That is strictly better than any of (a) anchor on the first face and be wrong on
a curved sheet, (b) sample a normal at the bbox centre and be arbitrary on a
folded one, or (c) require a face pick and change what the tool demands.
The arrow is refused when the picked face belongs to a DIFFERENT body than the
sheet in the combo. An arrow standing on one body while the tool acts on another
would name the wrong thing, which is worse than no arrow.
ThickenSurface was not on the audit's list — it is a distinct tool from Thicken,
with its own card and its own sheet-body combo, and it has exactly the same
shape. Fixing one and not the other would have left the same gap under a
different name.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-9fel.
detect_mate_conflicts() has been filling m_doc.mate_conflicts on every recompute
since the kernel half landed, and nothing read it. The diagnostics existed and
were invisible — a conflicting assembly looked exactly like a working one.
The tree row is where they go, because the tree is where the user is already
looking for which feature to change. Three states, in precedence order:
disabled -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it
must read as suppressed rather than keep shouting about it.
conflict -> warn.
otherwise -> normal.
Selecting a marked row puts the reason on the status line — "Mate3 already
positions Body 2", the cycle, the self-mate — and names the eye as the way to
suppress it. A message that describes a problem with no action is a message that
gets ignored; the action here is already one click away on the row just selected.
Deliberately NOT a modal, and deliberately not treated as a document error. The
document still evaluates with a conflict present: the mate graph merely has more
than one answer for a body, and which one wins is the thing the user needs to
see. Blocking the loop to say so would interrupt without helping.
The dimming of non-involved bodies from the original UX proposal is still not
implemented, on purpose: under transform composition a failure mid-chain
propagates, so "not involved" is not a well-defined set, and dimming the wrong
bodies would hide the context needed to understand the conflict.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bioq.
Two defects found by auditing the body-focus x-ray path, which shipped compiled
but never exercised. Neither is reachable from the happy path its test plan
walks, which is why compiling it proved nothing.
1. A STALE FOCUS KILLED THE VIEWPORT. The focus is a body INDEX held by the panel
across recomputes, so it outlives the body it names: delete a body and the
stored index can point past the end. body_pickable() then rejected EVERY body,
because none of them equals an index that no longer exists — a viewport that
silently accepts no clicks at all, with nothing on screen saying why. Out of
range now means no restriction. Fail open, never dead.
2. THE COMBO AND THE FOCUS COULD DISAGREE. refresh_cs_body_choice() rebuilds the
Body combo and, when the body list shrank, silently reset the selection to
"(all)" — while the viewport stayed focused on the old index. Every other body
kept its 25% alpha and picking stayed restricted to a body that might be gone.
That is the exact mirror of the open_tool ordering bug this feature already
fixed once: that one showed "Body N" over an opaque scene, this one shows
"(all)" over a dimmed one. They are one state and are now written together.
Guarded on CoordSys being the active tool, since it is the only card that owns
this focus. In the edit path the function runs BEFORE open_tool with the
previous tool still active, so the guard is false and the caller's explicit
set_xray_focus still wins.
Also confirmed while reading, since the header asserts it: set_solid_pick() does
NOT touch m_pick_only_body, so the focus really does survive the mesh feed. That
claim now has a check behind it rather than a comment.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bgvk.
Two open findings from the first rig judgement of the connector glyph.
F4 — the quadrant collapses to a blob at grazing angles, which is exactly when
the roll is hardest to read. Adds a radial tick along +X extending past the disc
rim. As the disc flattens to a line the sector loses all its area, but a radial
spoke keeps its length and its direction along the one axis that still projects.
The alternative on the issue was to billboard the quadrant while the disc stayed
in-plane. Rejected, and not on taste: at true grazing the view direction lies IN
the connector's plane, so every in-plane direction projects onto the same screen
line and the roll is geometrically unrecoverable. Billboarding would not recover
it — it would face the camera and read as a definite orientation that is not the
frame's. Degrading to a direction that can still be trusted beats drawing a
confident lie. The tick is additive, so unlike billboarding it cannot make the
non-grazing case worse; it still wants judging on the rig at a true grazing view
before F4 is called closed.
F5 — roll-undefined was a loud red: the strongest colour in the viewport spent on
the least important connector, pulling the eye off the mate being made. It marks
"this one could not be derived", not an error. Muted amber says look-here without
shouting.
No tick is drawn when the roll is undefined — a tick there would assert a
direction that does not exist, which is the silent guess the hatched quadrant
exists to avoid.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-wgsc.
Rib's depth already reused the Extrude arrow. Its thickness could not: the arrow
points along the plane normal, and thickness is an offset either side of the rib
line, IN the plane. Different direction, different handle.
Two square handles at mid ± perp·half, plus the slab's actual footprint drawn as
a thin closed rectangle — the footprint matters more than the dots, because what
a rib thickness means is how wide that slab lands on the body, and until now
there was no way to see it before committing.
A drag on either handle sets the FULL thickness, twice the perpendicular distance
from the line, because the slab is centred on the line and the handle sits at
half. Both handles behave identically for the same reason, so they share one
colour rather than pretending to be two different actions.
A zero-length line has no direction to grow a slab perpendicular to, so the
shared rib_frame() helper returns false and render and drag both draw nothing
rather than dividing by zero. Non-Line entities clear the gizmo instead of
guessing: the kernel is line-only and a gizmo that guesses would be lying about
what Confirm will build.
Unlike the helix callback this one goes through refresh_preview(), because Rib
builds a real solid ghost that has to rebuild. The helix has none and skips it
deliberately.
Both gizmos coexist and resolve the sketch and entity the same way, so the depth
arrow and the thickness handles can never disagree about which line they are on.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-plew.
Driving the control socket: hexagon prism, six vertical fillets, four chamfers
on the already-filleted rim, an M8 hole. Afterwards describe_scene reported
bodies=3 and error='' — entirely healthy — while body 2's TopoDS_Shape was null.
Only mass_properties on that one body revealed anything was wrong.
So a feature destroyed a body, recompute() returned true, and the document went
on advertising it. Any downstream consumer — slicing, STEP export, a mass
properties report — met a null shape with no warning. That is the silent
corruption class, which is the one class this project does not tolerate.
recompute() now scans the freshly built bodies for a null shape, names the body
and the feature that destroyed it, and returns false. Returning false rather than
just setting error is the point: it hands the caller its normal rollback path, so
the operation that destroyed the body is undone instead of committed.
The message says "an unidentified feature" when source_feature is -1. "feature 0"
would be a lie, and a message that exists to tell you where to look has to be
trusted.
TEST IS A POSITIVE CONTRACT, AND THE REASON MATTERS. The reported order was
driven headlessly first, as the better test: it does NOT reproduce. The dress-up
step throws "fillet radius too large", which is an already-loud already-caught
path, so recompute fails honestly and never nulls a body. No public-API sequence
found so far reaches the guard's branch without a GUI, and faking a null into
`bodies` after the fact would not exercise it — the guard runs on `built`, before
the swap. So the test asserts what can be asserted: a box + fillet recomputes
true, error is empty, and no body is null. The guard's own branch is defensive
and currently unexercised; that is stated here rather than implied by a green
suite.
Kernel suite: 2217 assertions in 161 test cases, all passing. No existing test
relied on a null body surviving a recompute, so hardening this broke nothing.
snaporca-5425 (part a). Part b — why the chamfer chain degenerates on an
already-filleted rim — is untouched and stays open.
grep -i helix over the viewport code returned nothing at all. The tool was four
coupled numbers and a Confirm button — you typed radius, pitch, height and taper
blind and pressed OK to find out what you had made. So this is not only the
charter's L2 failure; the tool had no visible state whatsoever while it was open.
Adds a plane-anchored helix gizmo built on the datum-plane gizmo as its template,
being the closest existing thing: also plane-anchored, also driven by a card while
the sketch tool is inactive, also a render / hit-test / drag triad.
It draws the live curve and the axis, and puts a handle on each of the three
lengths: radius on the base circle, height at the top of the axis, pitch at the
end of the first turn — which is exactly where one pitch of rise lands, so the
handle means what it is standing on. Below one full turn the pitch handle moves
to the end of the curve rather than floating off a curve that does not exist yet.
Taper and handedness stay on the card. One is a shape modifier and the other a
flag; L2 governs numbers you can point at.
A drag reports the whole (radius, pitch, height) triple rather than one value,
because pitch and height are coupled through the turn count and writing one alone
would redraw a stale curve. The callback re-feeds the gizmo directly instead of
going through refresh_preview(), since Helix takes the produces-no-solid early
return and refresh_preview would rewrite the status line on every mouse move.
REVIEW CATCH, fixed here: the first cut read taper as a fraction of the radius
consumed over the turn count. It is an ANGLE IN DEGREES — helix_spine() builds a
Geom_ConicalSurface of half-angle taper and takes the top radius as R+H*tan(taper),
growing with the height risen. The wrong reading drew a preview that collapsed to
a point for any non-zero taper while the committed feature was perfectly fine. A
preview that lies is worse than no preview, which is what this commit replaced.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
Rib's depth is a distance along the sketch plane normal, so it is the Extrude
arrow for the fourth time — anchored at the midpoint of the line the rib is
built on, because a rib's line IS its profile.
This is half of Rib's L2 failure. The thickness is an in-plane offset either
side of that line and no existing gizmo draws that; it needs a handle that does
not exist yet, filed as snaporca-plew rather than left implied. One of two
numbers draggable is strictly better than neither, and saying which half is
missing is the point.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
Both tools produce exactly one number — a distance along a known normal — and
neither had a handle for it. That is the same shape as the Extrude depth arrow,
which was already written, already draggable and already had an editable label
on the geometry. So this adds no gizmo: it points the existing one at two more
tools.
SurfaceExtrude anchors on its sketch's plane, at the profile centroid.
Thicken anchors on the picked face, and reuses the face-as-profile recipe from
the Extrude path verbatim — including the two things that path learned the hard
way: look the face up on its OWNER body rather than the whole-document compound,
and carry that body's display Move transform onto both the origin and the
normal, or the arrow draws on the bed instead of on the face.
The drag callback routes by active tool. `second` stays Extrude's alone: it is
the two-sided pair, and the other two have a single distance each.
SurfaceOffset is the third tool in this group and is deliberately NOT here. Its
target is an arbitrary sheet body, which has no single normal to anchor an arrow
on — that is a design decision, not typing, and it stays on the audit.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
The Placement > Transform verb opened a card of spin controls — dx/dy/dz, an
axis combo, an angle — with nothing on the geometry. The 3-axis drag gizmo the
charter asks for already existed and was fully implemented (arrows, rotation
rings, click-to-type per axis), reachable only from a small icon button in the
tree card header. The prominent verb opened the form; the geometry-first
control was hidden behind an icon. That was backwards.
Transform now arms that same gizmo on the target body. The card stays as L2's
typed half: the drag writes dx/dy/dz, the axis and the angle, and the pivot is
seeded from the body's centroid so the parametric feature reproduces exactly
what was dragged.
Decomposition is exact for the interaction that matters — the gizmo's rings are
per-world-axis, so a ring drag is an axial rotation. A pose composed from two
rings is not axial and the card can only name one axis, so it reports the
dominant one rather than refusing to answer.
Three things this had to get right:
- The gizmo bakes its drag into the display transform so the body follows the
cursor, and the feature performs the same motion parametrically. Committing
without reverting first would move the body twice.
- tool_confirm() and tool_cancel() both tested moving_body() BEFORE the active
tool, so with the gizmo armed Confirm would have dropped the gizmo and never
created the feature. Both are now guarded on Tool::None.
- close_tool() is the single revert point. Esc, Cancel and switching tools all
pass through it, so a Transform that was never committed cannot leave the body
displaced.
Edit mode is untouched: re-seeding the gizmo from a stored feature is a separate
problem, so editing an existing Transform still gets the card alone.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-qtf4.
Three defects behind one report ("bodies cannot be moved or hidden/shown or
deleted, colour does not work"). They are unrelated to each other; only the
symptom was shared.
1. The Color tool wrote a per-body override that was correct end to end —
stored on CadBody, carried across recompute (CadDocument.cpp:3381), read
back by DesignCanvas::body_color() — and then overpainted every frame.
m_body_selected is a DOCUMENT-WIDE flag raised whenever a non-Sketch
feature row is selected, which is the resting state after any modelling
operation, and while it was true every body rendered gold. An explicit
colour now outranks the selection tint; unpainted bodies still tint, which
is all the tint was ever for.
2. The eye toggle re-selected the body row through m_tree, using item ids that
belong to m_parts. The row came back unselected, so the second press found
tree_body_selection() == -1 and fell through to the feature-level branch
instead of un-hiding. Hide worked exactly once. The sibling call in
refresh_parts() had it right.
3. The tree card's Delete button answered a selected body row with "select the
FEATURE that created this body" — an instruction the user cannot act on,
because the tree does not say which feature that is. on_delete_body()
already resolves CadBody::source_feature and confirms by name; it was
reachable only from the right-click offer. The button now routes to it.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised — needs a session at
the machine to confirm all three in the viewport. snaporca-zjvg.
DesignCanvas::set_view() and fit_view() were both written and then never called
from anywhere in the tree. The Design viewport has had no way back to a standard
view since it existed: no key, no button, nothing but orbiting by hand until the
model happens to drift into frame.
That is worse than a missing convenience. A camera left pointing along the bed
plane renders a scene that looks exactly like a failed renderer — geometry
present, nothing visible — and an hour went into blaming the software GL stack
before the real cause turned out to be two uncalled functions.
Home rather than a letter: every letter A-Z is already a Shift+letter tool
shortcut. Home is also the reset-the-view key most users arrive with. The
dispatcher needed no change, it keys on the raw wx keycode. set_view() already
does select_view + zoom_to_volumes, so this is fit and orient in one call.
Doc row added to the View toggles table in docs/design_tab.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tests/libslic3r/CMakeLists.txt added only test_caddocument.cpp under
SLIC3R_CAD. The other five shipped in the tree and were never compiled, so
49 TEST_CASE blocks looked like coverage and were not: sketch constraints,
sketch editing, sketch import, inference, and the libslvs constraint set.
They also still targeted Catch2 v2 — mainline is on v3, where the umbrella
header is catch2/catch_all.hpp and Approx lives in the Catch namespace rather
than at global scope. Both fixed; nothing else in the files changed.
Found by building the tree rather than reading it. Suite goes from 374 to 423
test cases, 54,424 to 54,620 assertions, all passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two enabled mates driving the same body is not an error today — the later one
just wins, and the earlier mate looks ignored with nothing said. A cycle in the
mate graph is worse: composition still produces a result, but an arbitrary,
order-dependent one.
recompute() now fills a mate_conflicts vector of (feature index, reason) before
the geometry pass, so it survives a throw further down. It catches a second mate
on the same target body, a mate positioning a body against itself, and a cycle,
via an iterative three-colour DFS over the body graph. Broken mates are skipped
silently — apply_mate() already errors on those.
Deliberately non-fatal: recompute() still returns true and error stays empty.
Deliberately not "over-constraint" — that word promises DOF analysis from a
solver this kernel does not have.
Port of snaporca ec4ffeb979. Kernel half of snaporca-bioq.
Suite: 2213 assertions / 160 cases green on this fork too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
In an assembly the face you want for a mate connector is nearly always behind
another body, and a click only ever returns the frontmost hit. Hiding the
occluder from the Parts list works but means leaving the tool mid-pick.
The CoordSys card now carries a Body chooser. Pick a body and every other one
drops to 0.25 alpha AND stops catching clicks, so the wanted face is both
visible and reachable in one gesture. "(all)" restores normal picking.
Deliberately NOT hit cycling: repeated-click cycling was removed from solid
picking as a charter L5/§10 violation (DesignSketchTool.cpp, "NO CYCLE"), and
re-introducing it here would make "click a face" a multi-click gesture again.
Mechanics: DesignSketchTool::set_pick_only_body() gates body_pickable(), which
every pick path already consults; DesignCanvas::set_xray_focus() drives both it
and the per-body alpha in reload(). The chooser stays a pick FILTER only --
coordsys_body still comes from the actual pick, so nothing in the kernel moves.
Body focus follows the CoordSys card: open_tool() reads it back from the combo
rather than clearing outright, because editing a CoordSys feature loads the card
(and its body) before open_tool runs.
snaporca-bgvk. NOT COMPILED: deps/build lacks OpenVDB so the GUI tree will not
configure here; reviewed by diff only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mate_cs_a/mate_cs_b are feature indices. remove_feature() remapped sketch_ref
through the deletion but not the mate connectors, and move_feature() swapped
sketch_ref but not the mate connectors. Deleting or reordering any feature
ahead of a connector slid both references onto whatever features landed on
those slots.
Nothing reported it. recompute() only rejects out-of-range and non-CoordSys
targets, and a shifted index normally lands on the assembly's other CoordSys —
an assembly carries at least two by construction. So the mate resolved against
the wrong frames and moved the wrong body, silently.
Extracted a remap lambda in remove_feature() and a swap_ref lambda in
move_feature(), applied to sketch_ref and both mate connectors.
Two tests, both confirmed red before the fix. [mate] tags green here:
395 assertions / 29 cases — the first end-to-end kernel compile of this fork.
Ported from snaporca; CadDocument.cpp is byte-identical across forks again.
Refs: snaporca-kqih
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Move Generic vendor above Bambu vendor in AMS material setting.
* Remove hardcoded sorted_names. Alphabetically sort Bambu with all vendors
* Fix sorting with case insensitive comparison
* Use arithmetic to get rank distance because priorities are stored in a vector. This lets us remove the <interator> include.
* Move currently active filaments added to the Prepare sidebar to the top of the AMS Material Selection combo box.
It is likely the user wants to set the material to the currently active filament.
* Reduce logging verbosity.
* Refactor current active preset filament finding to find nested preset inheritance.
* Initialize pointer to null before usage.
* Remove old commit code
* Remove new line
---------
Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
* Fix detached copies of system presets
* Clarify detached preset compatibility
* Show unique preset state in save dialog
* Update SavePresetDialog.cpp
---------
Co-authored-by: yw4z <ywsyildiz@gmail.com>
Brings in 560 commits (merge base 2026-07-04 .. origin/main af9fd10d7a).
19 conflicts resolved; the other 138 touched files auto-merged.
Conflict resolutions
- Snapmaker/Polymaker (7): main normalised JSON key order in #15039 while this
branch inserted filament_id/filament_vendor. Took main's key order and kept
this branch's values, inserting a single filament_id key rather than letting
git's line merge leave duplicate "from"/"instantiation" keys.
- re3D rPETG @0.8/@1.75 nozzle (2): main renamed these from "re3D Greengate
rPETG @*" and re-vendored GreenGate3D -> re3D (#15169). Git's rename-aware
merge produced a file with two filament_vendor keys; took main's version
wholesale instead. The id is reconciled by the tooling, not by hand.
- re3D rPP (1): kept main's inherits change (fdm_filament_pet -> fdm_filament_pp,
local filament_type override dropped) and its widened compatible_printers.
The flattened type is still PP, so the product triple and id OFfHGM1D are
unchanged.
- re3D Greengate rPETG.json, Afinia {ABS+,ABS,PLA,TPU,Value ABS,Value PLA}.json
(7 modify/delete): accepted main's deletions. The Afinia ids are not orphaned
- the surviving @HS presets resolve the same triple and already carry the
same ids, so nothing is retired.
- .github/workflows/check_profiles.yml: kept both main's new "validate slice"
step and this branch's tree-wide filament-subtype check.
- tests/libslic3r/CMakeLists.txt: kept both test_filament_id_succession.cpp and
main's test_preset_diff.cpp.
Verified
- No conflict markers; all 12795 profile JSONs parse with no duplicate keys.
- All branch artifacts (tooling, snapshot, ledger, doc, tests) intact and
unmodified by the merge.
- The succession-ledger C++ call sites survived main's refactors; audited
Preset.{cpp,hpp}, PresetBundle.cpp, DeviceManager.cpp, PresetComboBoxes.cpp,
CaliHistoryDialog.cpp, MoonrakerPrinterAgent.cpp against base/ours/theirs.
- scripts/tests: 115/116 pass.
Known follow-up: assign_filament_ids.py --check reports 243 errors, all from
filament data main added in the last month (BBL/addnorth, Qidi X5/Plus 5,
Snapmaker U1, re3D). The single failing unit test is the live-tree conformance
test asserting that count is zero. Reconciliation lands separately.
* re:3D profile updates.
- Replace vendor-specific "re3D Greengate rPETG" filament with a generic "re3D rPETG" (base + @0.8/@1.75 nozzle variants), matching the naming convention used for rPLA/rPETG elsewhere in the re:3D vendor
pack.
- Add fdm_filament_pp as a proper filament-type parent and switch re3D rPP to inherit from it instead of overriding filament_type on top of fdm_filament_pet.
- Added filament_type to the specific printer JSON file and removed from the base printer JSON file [fixes Issue#14693]
- Updates to speeds and accelerations for re:3D profiles, moved from common to machine processes [closed: PR#14259]
- Updates to fdm profile filename format so that it lists the filename and extruder number in the sliced .gcode file
* Fix setting IDs
* Add rename from for changed material names.
Follow-up to #14785. Routes the tests that still hand-rolled temp paths through
the shared helpers and unifies the temp guards.
- Add ScopedTemporaryDir and a shared ScopedTemporaryPath base under it and
ScopedTemporaryFile.
- Move test_3mf's round-trip .3mf output out of the TEST_DATA_DIR source tree
(a fixed-name leak) and test_toolordering's fixed-name temp .gcode (a sharding
collision) onto ScopedTemporaryFile.
- Move test_config, test_slicing_pipeline_bindings, the test_3mf backup dirs, and
test_preset_bundle_loading onto the guards.
- Make slic3rutils ScopedDataDir compose ScopedTemporaryDir; dedupe
test_network_versions' fixture and delete test_plugin_lifecycle's duplicate.
* fix(GUI): honor "Ignore" when layer height exceeds the configured maximum
Entering a layer height above the printer's max_layer_height on the Print
Settings tab fired two guards in sequence. Tab::on_value_change prompts
"...Adjust to the set range automatically?" with an Adjust/Ignore choice, and
ConfigManipulation::update_print_fff_config then showed an OK-only "Too large
layer height. Reset to X" dialog whose result was never checked, so it always
reset the value. The second guard overrode the user's "Ignore", resetting the
layer height regardless.
Changes:
- Extract the shared Adjust/Ignore dialog into
ConfigManipulation::layer_height_out_of_range_dialog, reused by the tab
(Tab::on_value_change) and the per-object/part settings panels. The dialog now
names the value it will clamp to and reads correctly for too-low as well as
too-high.
- The tab/plate path is already covered by Tab::on_value_change, so the
duplicate reset is dropped from update_print_fff_config.
- The per-object/part panels have no on_value_change hook, so add
ConfigManipulation::check_object_layer_height and call it from the object
settings update paths, gated on the edited option (changed_opt_key ==
"layer_height"). It prompts once per layer-height edit and does not re-prompt
when unrelated object settings change after the user chose "Ignore".
Fixes#14214
* refactor(GUI): unify the layer-height range check across tab and object panels
Copilot review of #14369 noted that the per-object check only guarded the
max at extruder 0 and skipped the too-low case, diverging from the tab.
Move the whole range check into ConfigManipulation::check_layer_height,
used by both Tab::on_value_change and the per-object/part panels. It takes
the widest [min, max] window across the printer's extruders, offers
Adjust/Ignore in both directions, and resets a near-zero value. The tab's
inline block collapses to one call, dropping the duplicated limit logic.
* fix(GUI): only enforce layer-height limits that are actually set
max_layer_height defaults to 0 (unset), so the unconditional range check
offered to clamp any layer height to 0 on presets that don't define it.
Guard each branch (near-zero, too-high, too-low) so an unset limit disables
that direction; the slice-time nozzle-diameter check still applies.
Also run check_layer_height before update_print_fff_config in the object
panels so a near-zero per-object value prompts the same way the tab does,
with update_print_fff_config's fallback still covering the no-minimum case.
_update_select_plate_toolbar_stats_item(true) runs from
on_action_slice_all before the select-plate toolbar has necessarily been
initialized. m_all_plates_stats_item is only assigned in
_init_select_plate_toolbar, so slicing a multi-plate project shortly
after startup (before the Preview tab has rendered) leaves the pointer
null while show_stats_item is true, and the branch dereferences it,
crashing with SIGSEGV.
Every other dereference of this pointer already null-checks it. Add the
same check here so the all-plates stats item is left unselected until the
toolbar is initialized instead of crashing.
Fixes#15116
min_length_factor and is_top_or_bottom_layer had no default initializers, and the FillConcentric/FillConcentricInternal callers never set them, so WallToolPaths::removeSmallLines() thresholded on stack garbage. Which short extrusion lines it dropped then depended on memory layout, so concentric solid-infill output was nondeterministic between runs and across machines. Give every member a default, matching the adjacent FillParams. The perimeter path was already fine because it builds the struct via make_paths_params().
* Add the Qidi Plus 5
* Remove ignored profiles
Qidi didn't register these, so they are essentially dead weight.
* Set Qidi profile version to 02.04.00.10
* Review AI changes
* Part 2
* Part 3
* Part 4
God bless Ian Alexis
* Part 5
* Final part!
* Catches by Gemma
This 6-minute check probably saved me a week
* Tweak
* Update OrcaSlicer_ru.po
## Problem
`Toolchange temperature commands are unchanged when the wipe tower wait
is off`
(added in #15144) fails on both Linux runners and passes on Windows and
macOS.
It is the only failing test in the suite, and it has been failing on
main since
that PR merged.
| Job | Result |
| --- | --- |
| Windows x64 / Unit Tests | pass |
| Windows arm64 / Unit Tests | pass |
| macOS arm64 / Unit Tests | pass |
| Linux x86_64 / Unit Tests | **fail** |
| Linux aarch64 / Unit Tests | **fail** |
From the merge commit
([Linux
x86_64](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/31072382258/job/92531704095),
[Linux
aarch64](https://github.com/OrcaSlicer/OrcaSlicer/actions/runs/31072382258/job/92531704075)),
still reproducing on current main:
```
first difference at trace entry 29
main: M104 S240 T0 ; preheat T0 time: 31s lead 30.9s
branch: M104 S240 T0 ; preheat T0 time: 30s lead 30.3s
```
## Cause
Each preheat entry records the same quantity twice: `lead` at one
decimal, and
`time:` inside the command text as that value rounded to a whole second.
`split_lead` already compares `lead` with a 0.5s tolerance and explains
why the
estimate moves. `time:` sits in the exactly-compared command text, so it
never
got that tolerance — and being rounded, it flips on a drift far below
0.5s
(30.4 and 30.6 render as `30s` and `31s`). Entry 29 is the only entry in
the
163-entry golden whose lead rounds up; every other preheat sits at
30.0–30.4 and
rounds down, which is why it is the only one that fails.
The variation is per-toolchain, not run to run. Both Linux arches
produce
exactly `lead 30.3s`; Windows x64/arm64 and macOS arm64 all produce
exactly
`30.9s`. Repeated local runs are byte-identical. macOS arm64 passing
while Linux
aarch64 fails rules out the ISA — it is floating-point accumulation over
a few
thousand move durations under GCC vs Clang vs MSVC.
The mechanism makes it discrete rather than gradual: the backtrace parks
the
preheat at the first exported line at least `preheat_time` before the
tool
change, so `lead` is `preheat_time` plus the leftover of whichever move
that
landed on. A sub-tenth difference selects the neighbouring move and
`lead` steps
by that move's whole duration.
Entries 1–28 match exactly, including five earlier preheats whose leads
fall
inside the existing tolerance, so the toolpaths themselves are
identical. I also
reverted the two prime-tower commits that landed between the golden's
capture
point and now, rebuilt, and got a byte-identical trace — this is not
behavioural
drift.
That also rules out regenerating the golden: no single capture satisfies
all
three toolchains, and recapturing on Linux would turn the three
currently-green
runners red.
## Fix
Test-only.
- `lead` keeps a tolerance, widened to 1.5s (measured drift 0.6s; a
preheat
actually leaving its backtrace position would move by tens of seconds).
- `time:` is **not** compared across runs at all. Being a rounding of
`lead`, it
carries nothing the tolerance does not already cover, and comparing it
across
runs can only reproduce the flake. It is instead checked against its own
entry's `lead` — a correct rounding keeps `|time - lead| <= 0.5`.
That second point matters: simply tolerating `time:` numerically would
have made
the test blind to a real change, because drift and a wrong rounding both
move it
by 1. The self-consistency check keeps that coverage. I verified it by
changing
`(int) std::round(time_diffs[0])` to `(int) time_diffs[0]` in
`GCodeProcessor::export_lines` — the test fails with
`"time:" is not its entry's "lead" rounded to a whole second`, where a
plain
tolerance would have passed silently.
Everything else is still compared exactly: all M104/M109 values, tool
ids,
block markers, ordering, entry count, and the annotation text including
its
trailing `s`. The other 138 entries remain byte-exact.
No production code, no golden regeneration. The golden file and these
helpers
are used by this one test and nothing else, and the tolerance only
widens, so
Windows and macOS keep passing unchanged. A note is added to the
golden's header
so the next mismatch in those fields is not "fixed" by recapturing.
## How to verify
Before, on Linux:
```bash
git checkout main && ./build_linux.sh -t
ctest --test-dir build/tests -R "Toolchange temperature commands are unchanged" --output-on-failure
# fails at trace entry 29
```
After:
```bash
cmake --build build --config Release --target fff_print_tests
ctest --test-dir build/tests --output-on-failure # 463/463
```
Fix Windows crash in Replace all with 3D file
Keep the replacement result message as wxString and substitute the volume
name directly.
On Windows, wxString::ToStdString() cannot encode the Unicode status icon
through the active ANSI code page and returns an empty string. Passing that
empty string to boost::format with a volume-name argument throws
boost::too_many_args and exits OrcaSlicer.
* Fix redundant QIDI startup tool changes
Guard Q2, X-Max 4, and X-Plus 4 filament-change G-code so same-tool startup selections do not run the full cut, unload, and purge sequence.
* Guard Q2C against redundant startup tool changes
Skip the complete filament-change sequence when the requested tool is already selected during startup.
* Bump Qidi profile version
# 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?
-->
The prime tower reserved its footprint from the prime volume alone,
ignoring the flush volumes it actually has to hold, so on a multi-colour
print the tower shown in Prepare and the space kept clear for it during
arrange could be far smaller than the tower that gets sliced — leaving
it overlapping objects or running off the plate. This sizes the estimate
from the configured flush volumes instead, for rib walls as well as
rectangle and cone, applies the same height-based minimum depth the
prime-volume estimate already used, and reads the flush matrix correctly
on multi-nozzle printers, where it holds one block per nozzle.
Only the pre-slice estimate changes: the generated tower is untouched,
and prints that do not purge into the prime tower keep their existing
size.
# 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.
-->
<!--
> 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)
Prime towers reserved depth from the prime volume alone, ignoring the flush
matrix: rib-wall towers in both the engine and the preview, and rectangle and
cone towers in the preview, which never carried the flush-aware estimate the
engine already used. The preview also read the print preset, which does not
carry the printer- and filament-scope keys the estimate needs and so silently
fell back to defaults. On multi-nozzle printers the flush matrix, which holds
one block per nozzle, was additionally read as a single block. The tower could
come out too small for the purge it has to hold.
The flush-based estimate also skipped the height-based minimum depth that the
prime-volume one applies, so low-flush prints could estimate a tower shallower
than the one that actually gets built.
# 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?
-->
On delta, circular and custom bed shapes — and on multi-nozzle printers
where each carriage only reaches part of the plate — the prime tower was
positioned and clamped against the bed's bounding box, so it could be
parked in a corner the bed does not actually have. Neither the default
placement nor dragging the tower would pull it back onto the bed, and
slicing went ahead without complaint. The tower's default position, its
drag clamp and the slice-time validation now all follow the real
printable outline, and a tower that genuinely does not fit is reported
as "Prime Tower is partially outside the printable area" instead of
being sliced into a print that cannot be produced.
The travel that approaches the tower is planned against that same
outline. Previously the router gave up whenever its clearance box fell
outside the bed and drove the nozzle straight across the tower; a tower
parked near the bed edge now keeps its detour and enters through the
wall opening as intended.
This also corrects the footprint the prime tower validation uses for a
rotated tower, which was being rotated by the wrong amount and about the
wrong point, so proximity warnings and exclusion-area errors for rotated
towers were being computed against the wrong shape.
Prime tower placement on rectangular beds is unchanged. The new
printable-area validation and the tower-approach routing fix apply to
every bed shape.
# 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.
-->
Added unit coverage for the placement clamp against a non-rectangular
outline (a regular hexagon standing in for the shipped delta beds),
covering the rectangular-bed path, single-axis clamping while dragging,
a footprint already inside the outline, one sitting in the bounding-box
corner but off the bed, an unresolved auto brim width arriving as a
negative margin, and a footprint too large for the bed. The `fff_print`
suite passes.
<!--
> 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)
The placement clamps and the tower-approach router both stood in the bed's
bounding box for the bed itself, so on a delta or hexagonal bed the prime tower
could be parked in a corner that does not exist and the nozzle could be routed
across it. Both now test the real printable outline, slicing reports a tower
that does not fit instead of printing it off the bed, and a tower parked near an
edge is routed along the clamped side rather than falling back to a straight
line across the tower.
Also fixes the placement validation rotating the tower hull by degrees read as
radians about the plate origin, and never rotating the generated tower footprint
at all.
# 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 **Wait for temperature on wipe tower**, a printer option for
multi-extruder
machines using a Type 2 wipe tower. With it on, the new tool is picked
up without a
blocking temperature wait; the printer travels to the wipe tower and
waits there
right before purging, parked beside the tower so the ooze from the
heat-up lands
next to it rather than on the model. The incoming filament's target is
raised ahead
of the tool change, so the heat-up overlaps both the change itself and
the travel to
the tower.
The benefit is less oozing and less dead time. The tool no longer sits
at full print
temperature while it waits to be picked up or right after it undocks —
it heats on
the move and only reaches temperature once it is over the tower, so
there is far less
hot-and-idle time, and what does ooze ends up beside the tower. This
matters most on
tool changer printers with long docking and attaching cycles, such as
Tapchanger and
StealthChanger machines, where that wait is otherwise pure stall time
spent dripping.
The firmware or tool change macro must not wait for the temperature
itself. The
option is off by default and only shown for multi-extruder printers on a
Type 2 wipe
tower, and it is enabled by default for the generic toolchanger profile.
# 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.
-->
New Catch2 cases in `tests/fff_print/test_multifilament.cpp`: the wait
moves to the
tower when enabled, priming pre-heats to the first layer temperature,
the park side
is regenerated when the tower is moved or rotated, and a regression test
pinning the
unchanged (option-off) toolchange temperature commands against a
recorded trace
(`tests/data/wipe_tower_temperature_trace_main.txt`).
<!--
> 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)
Adds a printer option that picks up the new tool without a blocking temperature
wait, travels to the wipe tower, and waits there right before purging, parked
beside the tower so the ooze from the heat-up lands next to it rather than on the
model. The incoming filament's target is raised ahead of the tool change, so the
heat-up overlaps both the change itself and the travel to the tower.
Off by default, and only offered for multi-extruder printers using a Type 2 wipe
tower; the generic toolchanger profile enables it.
* fixes: memcpy(...) writing to an object of type OrientParams with no trivial copy-assignment; use copy-assignment or copy-initialization instead [-Wclass-memaccess]
* review result: replaces anonymous namespace with static
* fixes: may be used uninitialized [-Wmaybe-uninitialized]
* fixes: may be used uninitialized [-Wmaybe-uninitialized]
* fixes: may be used uninitialized [-Wmaybe-uninitialized]
* fixes: may be used uninitialized [-Wmaybe-uninitialized]
* reverts {} initializer to = to keep code style consistent
* fixes: %g directive writing between 1 and 13 bytes into a region of size between 6 and 18 [-Wformat-overflow=]
* fixes: %5s directive writing between 5 and 63 bytes into a region of size 58 [-Wformat-overflow=]
* fixes: catching polymorphic type by value [-Wcatch-value=]
* fixes: [-Wcomment]; removes whitespaces
* increases buffer size from 71B to 90B to avoid potential ovfl.
# Description
On Klipper the wipe tower's motion-queue synchronization silently did
nothing. Klipper acts on commands the moment it parses them, and its
`G4` reads only `P` in milliseconds — it ignores `S` — so the `G4 S0`
the tower used to flush the queue before a temperature change never
synchronized anything, and the cooling delay after a filament's cooling
moves passed instantly instead of waiting. The tower now emits `M400`
for the flush and `G4 P<ms>` for the dwell when the flavor is Klipper.
Only `gcode_flavor = klipper` is affected; G-code for every other flavor
is byte-identical, so no shipped profile or existing project file
changes.
# 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.
-->
<!--
> 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)
The wipe tower's "Delay after unloading" never happened on Klipper. It was
emitted as G4 S<seconds>, and Klipper's G4 reads only the P parameter, in
milliseconds, so the pause was silently skipped. The option now produces a
dwell Klipper actually performs.
Also corrects the planner flush rationale, which cited an extruder position
reset that Klipper resolves at parse time and does not need synchronized, and
adds end-to-end coverage that slices a two-filament print and checks the
emitted wipe tower G-code on both a Klipper and a non-Klipper flavor.
No change to any other firmware flavor's output, and no shipped profile sets a
non-zero delay, so no shipped profile's output moves either.
The wipe tower emitted G4 S0 to make the firmware finish its queued moves
before commands that must not take effect early. Klipper's G4 reads only the
P parameter, so that flush never happened there and a temperature change could
land seconds ahead of the moves it was meant to follow. Klipper now gets M400
instead, through one helper shared by both wipe tower implementations.
No change to any other firmware flavor's output, so no shipped profile or saved
project is affected.
A mate connector was visible only to a program. resolve_datum_coordsys had exactly ONE consumer
in the whole tree -- McpControl.cpp, the agent socket -- so the frame every mate is built on
could not be seen at all, and the two questions a connector has to answer on sight had no
answer in the viewport: which way does Z point (the VERSE), and which of the pair is anchored
versus about to move (the POLARITY).
The glyph is Onshape's proven core plus the part nobody ships. Disc for the XY plane, one gold
quadrant for the roll -- the only in-glyph answer to "where is X", which matters because
Fastened and Slider lock the clocking -- and a Z arrow drawn on +Z ONLY, never double-headed.
Polarity is carried by the head: a filled cone travels, an open collar receives. Onshape,
Fusion, Inventor and FreeCAD all draw both ends of a mate identically, which is why "which part
moves?" is a standing complaint; nothing here invents new semantics, it just stops hiding them.
Polarity is read from the committed Mate features, not only from the open card. A connector some
mate drives must read as driven whenever it is on screen, or the glyph tells the truth only while
a dialog happens to be open. The card, when open, still wins -- that is the live intent.
Judged on the rig rather than in a mock, which changed three decisions:
- Three RGB axis arms lose to one Z arrow. Rendered side by side (SNAPORCA_GLYPH=A selects the
Onshape-style trio), the three heads are as large as the 22 px disc, they bury the quadrant, and
at an oblique angle they pile into a smudge -- and the trio is indistinguishable from the move
gizmo and the bed triad, which are already RGB arrow trios in this viewport.
- Depth off floats, depth on tears. With GL_DEPTH_TEST off, connectors on faces pointing AWAY from
the camera drew their discs over the solid, so the part looked covered in frames that were on its
back. Turning depth on fixed that and immediately z-fought: the disc is exactly coplanar with its
face and came out a broken dotted arc. Depth ON plus a 0.7*upp lift along Z buys both, and scaling
the lift by upp keeps it sub-pixel instead of opening a visible gap on zoom-in.
- Foreshortening degenerates an arrow into a dot when the axis points at the camera. It now draws a
ring instead of silently vanishing, which is what a naive projection does.
Everything is sized in screen pixels via upp = 1/zoom, like every other gizmo here: a connector is
a symbol, not a part, so it must not shrink with the model.
Research and the empirical findings are written up in DESIGN_MATE_CONNECTORS.md section 8b;
rig images in artifacts/shots/g-0*.png, vendor reference glyphs in artifacts/glyphs/.
Not fixed here, and recorded rather than papered over: the quadrant collapses to a blob at a
grazing angle, which is exactly when the roll is hardest to read (F4); roll-undefined in red makes
the least important connector the loudest thing on screen (F5); and a true grazing view, a curved
face, and overlap with the move gizmo are still untested. Also surfaced while testing and unrelated
to drawing: add_mate accepted a mate between two connectors on the SAME body and duly transformed
the body relative to itself -- a concrete instance of the missing validation already filed as G6.
Fork parity unchanged: DesignCanvas.cpp 16, DesignPanel.cpp 30, the other four files 0.
* update snapmaker profiles. largely ported for Snapmaker Orca fork
* update prime volume
* set precise_outer_wall to 1
* Update per-material multi-tool ramming to the filament library
* Add per-filament overrides for toolchange retraction
* Set toolchange retraction per filament for Snapmaker U1
* set default support type to tree
* format snapmaker profiles
The linear approximation used a heuristic segment count clamped to 4..16, so the
lift ran as a coarse polygon. Every vertex is a direction change large enough to
hit the firmware's jerk limit, forcing a decelerate/accelerate at each corner —
the lift micro-stutters instead of running at speed. The segment count now comes
from the chord deviation against the slicing resolution, reusing
Geometry::ArcWelder::arc_discretization_steps, which keeps the turn at each
vertex shallow enough for the firmware to carry speed through the whole move.
Points are emitted through GCodeG1Formatter so they carry the same quantization
as the rest of the G-code, and the move comment now trails the feedrate line to
match _travel_to_z and the G2/G3 branch. No change when arc fitting is enabled.
* fix: make the error dialog caret point at the character it's blaming
Custom G-code parse errors print the offending line with a '^' under the
character that broke, positioned with spaces so it only lines up in a
fixed-width font. Since v2.3.2 these dialogs rendered entirely in the
proportional UI font, so the caret drifted left of its column and landed
on unrelated text.
Render only the code excerpts (the offending source line and its caret) in
the fixed-width face, leaving the surrounding prose in the UI font, and
reserve the horizontal scrollbar's height so a long line does not clip.
Rename the flag to has_code_excerpts to match what it now means.
Fixes#14869
* refactor(GUI): use <code> instead of <tt> for error excerpts
wxHTML maps <tt>, <code>, <kbd> and <samp> to the same fixed-width
handler, so this renders identically. <code> is the non-deprecated
tag and matches what the original code used.
* fix(GUI): align the error caret with real spaces, not
The caret line was padded with so its spaces would survive inline
HTML. wxHTML measures every glyph by its font extent, so where the fixed
font lacks a U+00A0 glyph the fallback renders it about twice as wide, and
the all- caret line outran the source, drifting the ^ to the right.
Wrap the excerpts in a small <excerpt> tag, registered on the dialog's own
parser, that switches on wxHTML literal-whitespace mode so the caret uses
real spaces that match the source column in any font. It sits inside <code>
for the fixed face; <pre> would do both but forces a blank line above it.
---------
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
M620 is Bambu firmware dialect, not a
neutral command. Composing it in
MachineObject let non-Bambu agents
(Moonraker/Klipper) forward it and
report success on firmware that
cannot run it.
Agents now own the dialect: the
default refusal on IPrinterAgent
returns not-supported so the UI
can say so; BBLPrinterAgent keeps
the byte-identical composition.
Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.
set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.
A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.
Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.
set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.
A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.
test(libslic3r): replace the disabled convex_hull_2d test, closing #11269
The last "failing libslic3r test" from #11269 was the disabled
SCENARIO("2D convex hull of sinking object", "[3mf][.]") in test_3mf.cpp.
It checked ModelObject::convex_hull_2d for a sinking object against
PrusaSlicer's reference hull, but Orca's convex_hull_2d does not clip
geometry below the bed the way PrusaSlicer's its_convex_hull_2d_above does,
so the reference never matched. The test also wrote a debug mesh to a
hardcoded /tmp path and its comparison loop was inverted.
Remove it and add tests/libslic3r/test_model.cpp characterizing
convex_hull_2d on non-sinking transforms (identity and scale+offset),
where the projected footprint is unambiguous. Homed in a Model test file
since it exercises ModelObject, not 3MF.
snaporca-200 asked which of the two models of "the card's face input" is right,
because clicking empty canvas now clears the selection (snaporca-od0) and made
them visibly disagree: Thicken / Shell / Draft read the LIVE selection and their
label reverts to "(pick a solid face)", while Hole / Thread LATCH the face they
were opened or picked on and keep it. The complaint was that Hole then drills a
face you can no longer see selected.
Taken to the rig, that turns out to be the wrong half of the story. With Hole
open and its face picked, a click on empty canvas leaves the Ø6.0 ghost and its
dimension gizmo drawn on that exact face — the card was never operating in
secret, it was showing its target the strongest way a CAD tool can. Meanwhile
Draft, whose behaviour was held up as the honest one, threw the pick away and
had to be told the face again.
So neither model replaces the other. They are different in kind: Thicken /
Shell / Draft are operations whose operand IS the selected face, and Hole /
Thread are placement tools with their own plane state that a pick merely seeds.
The latch is also the kinder of the two now that empty clicks are a deliberate
gesture — a stray one costs Thicken a pick and costs Hole nothing.
What was genuinely missing is that nothing in those two cards NAMED the latched
face, so after such a click the only words on screen were the viewport's
"Nothing selected" over a ghost about to drill. Both cards now carry an "On
face" row, the way the other three already do:
Hole Face 5 | (none — uses Hole plane)
Thread Face 1 | Edge 2 | (none — uses Thread plane)
Thread names an edge when the cylinder came from a circular rim rather than a
cylindrical face, which the code already distinguished internally and never
said out loud.
Verified on both rigs, every state driven through the GUI: face pick on open
and on live pick, survival across a click on empty canvas, and the fallback
after choosing XY/XZ/YZ from the plane dropdown. Thread's edge branch was
exercised on a revolved tube's rim, its face branch on the same tube's outer
wall.
Filed while here, surfaced by the new row rather than caused by it —
snaporca-uif9: re-editing a stored Hole/Thread from the feature tree restores
f.plane into the dropdown but never clears m_hole_on_face, so the re-edit
silently reuses the PREVIOUS card's latched face. Now visible by name instead
of invisible.
Fork parity unchanged: DesignPanel.cpp 30, DesignPanel.hpp 0.
Both found by Kimi reviewing the previous two commits, both then reproduced here
before being touched.
snaporca-97z. The re-pick escalation required m_solid_sel, m_sel_body, m_sel_face AND
m_sel_edge to all match the previous pick. That looked stricter and was wrong: the
edge branch sets only m_sel_edge and m_solid_sel, leaving m_sel_face as whichever face
the ray happened to enter through — and a shared edge is entered through a different
face depending on which side you view it from. So picking an edge and picking that
same edge again from the other side compared equal edges, unequal faces, and refused
the escalation the status line had just promised. Now the comparison is made at the
level that was picked and nothing else. The edge id is already the stable global one
from edge_index_of, so it identifies the edge without help from the face.
Reproduced on the rig without needing to orbit, since two clicks 8px apart across an
edge enter through different faces:
pick -> sel=3 body=0 face=5 edge=3
ray -> body=0 face=0
re-pick -> escalated to whole body 0
pick -> sel=1 body=0 face=-1 edge=-1
Same edge, face 5 then face 0, escalation fires. The old condition could not.
The frame-move case. The chip is anchored at an absolute screen position, and until now
nothing told it the window had moved — only a resize, a status change or a tab switch
re-placed it. Dragging the window by its title bar left it stranded where it was,
verified on the rig by moving the frame and watching it stay put. wxEVT_MOVE on the
top-level frame, alongside the ICONIZE and ACTIVATE binds from the previous commit.
Two related cases are filed rather than bound, because the list of window-geometry
events to chase is exactly what snaporca-lcq argues should stop: a layout change that
translates the canvas without resizing it, and wxEVT_DPI_CHANGED.
Not fixed, deliberately, and recorded on snaporca-97z: clicking the same FACE but
landing within the vertex or edge tolerance resolves to a different kind and so does
not escalate — that is the "smallest thing under the cursor" rule working as
documented; and a vertex re-pick after moving the body compares stale world
coordinates.
Verified on both rigs. The cross-face edge case was exercised on orca_cad;
DesignSketchTool.cpp is byte-identical across the forks, so snaporca inherits it, and
its face-level escalation and empty-click clear were re-checked there directly.
Fork parity unchanged: DesignSketchTool.cpp 0, DesignCanvas.cpp 16.
Found by minimising the app on the rig with a face selected: the whole screen goes
black and the chip is still drawn on the bare desktop. A wxPopupWindow is
override-redirect — the window manager does not own it — so it neither iconises with
its frame nor stacks behind other applications. IsShownOnScreen does not catch this
either: an iconised frame still counts as shown, which is why the guard added for the
tab case sails straight past it.
So the frame has to say so itself: ICONIZE and ACTIVATE, both routed through the same
show_status_hud the page change already uses. Restoring is safe — a popup cannot take
focus, so our own Show() cannot re-trigger either event — and restoring while some
other page is up still leaves the chip down, because show_status_hud(true) goes
through place_status_hud's IsShownOnScreen guard.
Verified on both rigs: chip up, minimise -> screen black and empty, restore -> chip
back with its text and the face still selected. Restore while on Prepare -> chip stays
down.
This is the fourth defect from the same root, so snaporca-lcq now asks the question
these binds keep deferring: whether the line should be canvas content, like the view
cube and the round view buttons, rather than a window that has to be told about every
way a window can stop being visible.
Fork parity unchanged: DesignCanvas.cpp 16.
* Fix gizmo being closed after releasing mouse outside the gizmo floating window
The left up event of a drag started on the gizmo floating window (e.g.
selecting text in an input field) and released over the bed was treated
as a click on the plate, which deselected the objects and closed the
active gizmo. Add the ignore_left_up guard to the plate select branch,
matching the deselect branch above.
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix Emboss gizmo being closed after releasing mouse outside its floating window
The Emboss gizmo has its own close-on-click-away handler
(on_mouse_change_selection) that was not protected against left up events
originating from ImGui windows, so the gizmo was still closed when a drag
started on its floating window (e.g. selecting text in the input field)
ended over the 3D scene. Expose the canvas's ignore_left_up state to
gizmos and skip the close check for such releases.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* imgui: Clamp mouse y-coordinate in multi-line click/drag to text bounds
In single-line mode, click and drag already clamped y to the line's
y-coordinate so the cursor would continue to follow the x-position when
the mouse went off the top or bottom of the text. Multi-line mode did
not clamp, so stb_text_locate_coord() would return 0 (above) or n
(below), snapping the cursor to the very start or end of text and
ignoring the x-coordinate entirely.
Now both modes walk the row layout to compute the top of the first row
(y_min) and bottom of the last row (y_max, minus half a line height to
add tolerance for rounding), then clamp y to that range before passing
it to stb_text_locate_coord(). This means dragging or clicking above
the text now places the cursor on the first line at the x-coordinate,
and dragging/clicking below places it on the last line at the
x-coordinate, matching the single-line precedent.
* Fix issue that cursor cannot be placed at the last empty line
Two things a click on nothing should already have done.
snaporca-od0. A click that hit no geometry left the solid selection standing. A
rubber band swept over empty space has always cleared it (pick_bodies_in_rectangle),
and the two gestures cannot disagree about the same outcome. The visible cost was in
the escalation that landed last commit: "click the face, click away, click the face
again" arrived as the SECOND click on the same face and took the whole body, when the
click away was the user letting go of it. Now the miss clears and says so.
This gives up something real, deliberately: Thicken / Shell / Draft hold their input
face in the panel's selection, so a stray click on empty canvas with one of those
cards open hands that face back. Their handlers already write the "(pick a solid
face)" placeholder and rebuild the ghost when the selection empties, so the card SAYS
it lost the pick rather than confirming against a face the viewport has stopped
highlighting. An orbit drag never reaches this branch — it exits at the 8px budget —
so panning the view still does not deselect.
snaporca-dlj. The status line is a wxPopupWindow, which is a TOP-LEVEL window: hiding
the Design page does not hide it. Select a face, switch to Prepare, and the chip was
still there reading "selected (whole body) — right-click for what applies to it" on a
tab with no such selection and no such menu. Same cause, second symptom: a status
update arriving while the page is hidden anchored against a client size that is not
the size the page will have, and parked the chip on the tab bar. So: an
IsShownOnScreen guard in place_status_hud, show_status_hud(bool) to take it down and
bring it back with its text intact, driven from the page-changed handler.
Verified on both rigs, not by reasoning about it: face 5 selected -> click bed ->
"Nothing selected", tint gone -> click the same face -> face 5 again, NOT the body ->
click it again with no click away -> whole body, so snaporca-gem is intact. Prepare ->
chip gone; back to Design -> chip returns. KEYTRACE across the round trip shows
shift+S then R still reaching the canvas (ui_mode 0 -> 1, Rectangle armed), which is
the focus theft this popup replaced a wxFrame to avoid.
Fork parity unchanged: DesignPanel.cpp 30, DesignCanvas.cpp 16, headers and
DesignSketchTool.cpp 0. MainFrame.cpp is outside that set and was edited per fork.
On Linux the bundled CPython silently links the system OpenSSL instead of
the one built in deps/, and the dependency build then fails:
install: cannot stat 'Modules/_ssl.cpython-312-x86_64-linux-gnu.so':
No such file or directory
The chain:
* OpenSSL's linux-x86_64 target sets multilib=64, so 'make install_sw'
installs the static libs to <prefix>/lib64 while every other dependency
in the prefix uses <prefix>/lib.
* CPython's --with-openssl=<dir> only ever emits -L<dir>/lib. It does not
look in lib64, so -lssl resolves to the system OpenSSL.
* gcc -shared does not error on unresolved symbols, so the link appears to
succeed. _ssl.c was compiled against the bundled 1.1.1w headers, which
map SSL_get1_peer_certificate onto the pre-3.0 SSL_get_peer_certificate
-- a symbol OpenSSL 3.x removed. The module then fails to import:
_ssl failed to import: undefined symbol: SSL_get_peer_certificate
Could not build the ssl module!
* With no _ssl built, 'make install' cannot stat it and the build stops.
Passing --libdir=lib keeps the prefix single-layout, so CPython's -L<dir>/lib
finds the bundled static libraries and links against the headers it was
compiled with.
CMake-based dependencies were unaffected throughout, because CMake's
FindOpenSSL searches lib64 on its own; only CPython's autoconf path is
sensitive to this.
Affects any distribution where OpenSSL selects the lib64 layout, which is the
Fedora, openSUSE and Arch families. Debian and Ubuntu are unaffected, which is
why CI has not seen it.
Verified on Arch (GCC 16.1.1, CMake 4.4.2): the dependency build completes and
the bundled interpreter reports the bundled OpenSSL rather than the system one:
$ deps/build/OrcaSlicer_dep/usr/local/libpython/bin/python3.12 \
-c 'import ssl; print(ssl.OPENSSL_VERSION)'
OpenSSL 1.1.1w 11 Sep 2023
Not verified on macOS or Windows. The flag is accepted by OpenSSL's Configure
on all platforms and Darwin targets do not set multilib, so it should be a
no-op there, but CI is the check.
* Remove titlebar from splash screen on Wayland
* Broadly check for window decorations and added explanatory description
* Fixed hiding title bar on Wayland for all desktop environments
* Update format
---------
Co-authored-by: noisyfox <timemanager.rick@gmail.com>
A click could point at a face, an edge or a vertex, but never at the body those
belong to: offer_selection_kind() can only return BodySolid when all three are
clear, which a viewport click never produces. The rubber band was the only door,
and the status line said "face 5 selected" while the user believed they had taken
the body. A second click on the SAME sub-element now escalates to it (snaporca-gem).
Not the pick cycle that was removed in bc2b741ce9 -- that one was silent and three
deep, so no click had a predictable meaning. Here the status line names the next
click before you make it, and a further click just takes the face under the cursor
again, which needs no teaching. Double-click is untouched: wx sends Down/Up/DClick/Up
and only the first Up carries a pending press, so a fast double-click still zooms to
fit and picks once.
The status line itself moved to the base of the viewport. In the side panel it was
clipped at ~73 characters with no warning and no wrap -- set_status()'s Wrap() never
took effect (snaporca-8cc) -- which silently length-limited every hint in the tab; the
first version of this change lost a clause to it. m_status is kept, hidden, as the
owner of the text and its colour, and the line is drawn in a bottom-left twin of the
readout HUD where there is a whole window's width.
Three defects found driving it on the rig, none of which the build could see:
* the HUD as a wxFrame took the WM's keyboard focus every time it was raised, and
the canvas then received NO key events -- every sketch shortcut silently dead.
Caught with SNAPORCA_KEYTRACE: shift+S logged a line, the following R logged
nothing. It is a wxPopupWindow now, which cannot be focused. SetFocus() on the
canvas does not fix it: focus was on another toplevel.
* zero vertical padding fits the popup tighter than the font's line box and clips
the glyphs; 6 (what the readout uses) reads as a two-line box. 3 is right.
* "has a caller chosen a colour?" compared the label's foreground against its
PARENT's, which differ by default, so every line counted as chosen and the
neutral text came out the panel's dark grey -- invisible on a dark chip. Compare
against the colour the label was created with, captured before any caller writes.
Verified on both rigs against fresh binaries: sketch -> extrude -> click face ->
click again -> whole body tinted, offer opens with the body rows live and Create /
Add material correctly greyed. Keyboard drives the whole sequence.
Filed and NOT fixed here: snaporca-od0 -- a bare-plate click does not deselect the
solid, so "click away, click back" escalates. Pre-existing; clearing there would also
drop the face the Thicken/Shell/Draft cards hold, which needs its own pass.
Refs: snaporca-gem, snaporca-8cc, snaporca-od0
The third door onto the offer, after the viewport right-click and the Menu key. A body ROW is
an unambiguous body, so the offer reports BodySolid and the body verbs act on the row you can
see highlighted — the confirmation a face pick cannot give, since pointing at a face lights the
face and never the body the verb will change. The status line has been promising exactly this
("Body N selected — right-click for what applies to it") since before any handler existed on
that list; the product was advertising a gesture that did nothing.
WHAT THE RIG CAUGHT THAT THE BUILD DID NOT. The first version hung the state normalisation off
wxEVT_TREE_SEL_CHANGED. But SelectItem() on a row that is ALREADY selected fires no selection
event, so a stale vertex from an earlier viewport pick survived — and offer_selection_kind()
tests vertex FIRST, so right-clicking the body row served the VERTEX offer while the row sat
highlighted: Fillet/chamfer/draft greyed, Mirror standing where Repeat belongs, "vertex
selected" still in the status line and the cyan marker still on screen. The happy path (fresh
row, nothing else picked) looked perfect, which is why only the deliberate stale-state sequence
exposed it. Reading the code would not have shown it — SelectItem looks like it selects.
So the normalisation is no longer a selection handler. apply_body_row() is called
UNCONDITIONALLY by both doors, because taking a body from the list means the same state change
however it was asked for. It also clears m_sel_solid_vertex, which the original handler never
did — latent while nothing opened the offer from that list, and immediately fatal once
something did.
Verified on both rigs with the failing sequence itself: pick a vertex, then right-click the
already-selected row. Fillet/chamfer/draft enabled, Repeat back in place, status reads "Body 1
selected", vertex marker gone.
Does NOT touch the feature tree. That needs new selection kinds (offer_selection_kind has no
notion of "a feature is selected") plus verbs the atlas does not contain — Suppress, Rename,
Reorder, Roll back — and is filed separately.
Tommaso: "i deleted a body using rubber band selection, but this is not intuitive as all
the ux revolves around clicking". Correct on both counts, and a correction to what I said
last round: the rubber band IS implemented and shipping (pick_bodies_in_rectangle, m_rubber,
the drag branch in on_mouse). What is unbound is whole-body picking via CLICK; I read the
comment about the click path and wrongly generalised it to the gesture as a whole.
The handlers were never the problem either. Move, Mirror, Cut, Mass and Colour all resolve
their target through selected_body_default() / m_sel_solid_body, and that is already set when
you click a FACE — level >= 1 records the body. They would have worked from a click all
along. The only thing keeping them out was the atlas gate: accepts listed body_solid and no
face kind, so offer_selection_kind() returning FacePlanar filtered the rows away. This is
therefore an atlas-only change, no handler edits.
Cut, Split, Mirror, Transform, Mass and Colour now accept face/edge/vertex as well, matching
what Delete Body already did. Edges and vertices are included deliberately, not just faces: a
click resolves to a vertex, an edge or a face depending on where inside the pixel it lands,
so accepting only faces would make Move vanish whenever you clicked near a corner — a flicker
that reads as a bug and gets reported as "sometimes it works".
NOT widened: Extrude on a face means push/pull THAT face, and Thicken consumes the face you
point at. Both have genuine face-specific meaning, so widening them would change what they
do rather than where they can be reached from.
The rubber band keeps its job — it is still the only way to take a body without also naming
one of its faces. It just stops being the only door.
Verified on both rigs from a plain face click: Transform > Move opens with Body = Extrude2
(resolved from the face pick), Modify > Edit / Delete Face / Colour / Delete Body, and
Reference > Mass.
Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent
faults stacked behind that.
FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound
(DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields
Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but
no face kind, so the row was filtered out entirely — while the status line read "Body 1
face 0 selected", which actively teaches the wrong model.
SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the
FEATURE that created this body". CadBody had no link back to its maker, so the offer was
advertising a verb it could not perform — worse than the action:null rows fixed earlier this
session, because this one is ENABLED and its refusal reads like user error.
CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop,
and the rule is just "still unset?". That is sufficient because of an invariant worth
stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape
(boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the
stamp it was born with; a consumed body is erased outright, taking its stamp with it; and
the only bodies still at -1 are the ones the current feature just pushed. A feature type
added later needs no change here as long as it keeps to that invariant.
"Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by
pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the
body kinds so both can never appear for one selection. Deleting a body removes the feature
that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a
body vanishing from the viewport is not evidence of which feature went, and this is the one
action here that cannot be eyeballed.
Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler
deletes exactly one body, so a two-body selection would have silently deleted whichever was
m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild.
Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete
Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint ->
Undo -> Extrude2 and Body 1 restored.
The previous commit cites snaporca-y7q, which does not exist — I wrote the ID from memory
instead of reading it back from the bug I had just filed. The real one is snaporca-kgx,
"Offer: Thicken (and peers) open with the picked face discarded". The comment is corrected
here; the commit message above it cannot be, so this note is the pointer.
snaporca-y7q. Thicken's opener cleared m_sel_solid_face outright. That was right when the
only door was a toolbar button — a button carries no selection, so pressing Thicken had to
clear and ask you to point at something. The offer inverted it: the verb is now invoked ON
a face, and the same line threw away the only thing the user had said. The card opened
reading "(pick a solid face)" over an immediate "thicken: face not found" — you pointed at
the face and were told none could be found.
Keep the pick when the body combo landed on the body it came from (the index is per-body,
and selected_body_default() returns exactly that body when it is valid).
Two neighbours had the mirror-image flaw, both invisible for the same reason — the value
was right and the ghost updated, so only the label lied:
- Thicken had NO live label update at all. Nothing outside the opener ever wrote
m_thicken_face_label, so while the card was open you could pick face after face and it
still read "(pick a solid face)".
- Shell and Draft wrote theirs ONLY from the pick handler, which runs while a card is
already open — so opened from a selection they showed the previous pick, or the
placeholder over a face they were about to use.
So the label is now written once in open_tool(), which every door goes through. The
edit-feature path already restores m_sel_solid_face from the stored feature BEFORE calling
open_tool, so it agrees rather than fights.
Verified on the snaporca rig: face 4 of an extruded plate, offer > Add material > Thicken
now opens "Face: Face 4" with "Preview — 24 triangles" and confirms to a real Body 2. Draft
opened from a face shows "Face 3" and previews the taper. This fork is code-identical here
bar the two permitted DropDown divergences; it still owes a build of its own (snaporca-5pl).
Project keeps its clear: there "(all edges)" is a legitimate default mode rather than a
failure, so changing it would alter behaviour with no reported problem behind it.
snaporca-7ih's remaining half. Both flyout factories registered their verbs INSIDE the
widget-building loop, so the ~40 retired tool buttons had to be constructed and then
Hide()n: skipping construction would have deleted 42 offer verbs (26 fly:<family>#<row>
+ 16 Shift+keys) while their rows still rendered and did nothing when picked.
Register first, build second. The addresses are pure data; the widget is one door onto
them, not their owner. A family absent from kBarKeep now returns before any wxWindow is
made. The keep-list stays a one-line data decision, not a structural one.
And close the class of bug for good: the constructor now verifies, once, that every verb
the atlas marks wired resolves to a real registration, logging each break and asserting in
debug. Rows that render and do nothing have shipped three times (edit_feature and sk_move
with action:null, then this) and are invisible from either side alone.
Verified on the snaporca rig by walking the offer, not by reading the code — all four
at-risk address kinds run with no widget behind them: fly:design_rect#2 drew an OBLIQUE
rectangle (the third variant, not the family's first), key:S+E opened Extrude with its
10 mm gizmo, fly:material#4 opened Thicken. Hover hints, icons and nesting intact. This
fork is code-identical here bar the two permitted DropDown divergences; it still owes a
build of its own (snaporca-5pl).
Two hints were wrong and are fixed: Cut said "Split the body with a plane", colliding with
the Split verb one row away and pointing at a card for a value the canvas already offers as
a draggable arrow; Split never said its plane comes from a picked face.
Also, because it blocked the verification and will block the next one: gui-session.sh
killed by full path while its own app_pid() matched by basename, so a differently-pathed
instance survived, held the single-instance lock, and got reported as a healthy session —
a Jul-30 binary nearly passed as this build. It now kills by basename and prints which
binary is actually on screen. Traps 6 and 7 documented.
Mirror of snaporca 2b3e890165 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).
All 86 verbs now carry a hint: 55 extracted from the C++ tool definitions so the offer and
the armed-tool hint cannot drift, 31 written by hand. One wxEVT_MENU_HIGHLIGHT binding
shows the hovered verb's hint in the status line. The generator asserts that no wired verb
lacks one.
Also: all 200 status writes go through set_status(), which wraps instead of clipping at the
panel edge; and the empty-document hint is called from on_tab_shown() as well, since
after_tree_edit() never runs on a freshly opened tab.
Verified on the rig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 033347d062 (parity 30 / 16).
Retiring the toolbar made ten hints untrue: each named an action whose door had moved to
the offer, or a button no longer on the bar. They now name the gesture. An empty document
blanked the status line entirely and now says how to start.
Known and not fixed here: m_status does not wrap, so long hints clip; and the 86 offer
verbs still have no per-verb hint of their own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca b3d4cf85af (parity 30 / 16).
Hiding it with the drawing tools was wrong: Construction is a persistent MODE, not a tool —
the Bed checkbox, not the Line button. Q and the offer's Construction row kept toggling a
checkbox nobody could see, so you could not tell whether the next line would be construction
geometry.
Scoping unchanged and already correct: m_tb_sketch is shown only in UiMode::Sketch, so it
appears exactly while a sketch is open or being edited.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 809aa9df87 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).
fadd() and sadd() now gate what reaches the bar: file operations, Bed, Undo/Redo, Delete
selected, Commit to Plate, Confirm/Cancel, plus Place on Face and Section view — the last
two because they are chrome_only in the atlas and have no offer row to fall back on.
The tool buttons are still built and then hidden, deliberately: their fly: addresses and
Shift+key bindings are registered inside the widget-building loops, so not building them
would silently drop 42 verbs from the offer while they still rendered. snaporca-7ih covers
hoisting the registrations so the construction can go too.
Four separators whose groups are now empty were dropped; they rendered as stray rules.
Verified on the rig in both modes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca bcab67f8ce (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).
tool_atlas.json now names an icon for 80 of 86 verbs, derived from the toolbar's own
definitions rather than invented, and every one of the 54 distinct names was checked to
exist in resources/images first.
The first attempt drew nothing despite a green build: wxGTK builds the GtkMenuItem inside
Append() and reads GetBitmap() there, so setting the bitmap on the returned item is a
silent no-op. append_offer_item() constructs, sets, then appends — the same order Orca's
own append_menu_item() uses.
Verified on the rig.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 1fb7786d9a (DesignPanel.cpp and DesignCanvas.cpp applied as patches;
parity 30 / 16, shared files byte-identical).
With a sketch open, Text/SVG outlines become ordinary Line entities via push_closed_lines()
instead of a separate Sketch feature carrying rigid imported_regions — so the letters can
be constrained, trimmed and extruded like anything drawn by hand. The buttons and offer
actions arm Select first when in Sketch mode, since begin_sketch() does not run until a
tool is armed.
add_imported_regions() calls reset_autoedit(): without it the glyph contours entered the
draw-then-edit queue and opened a Length field on the first segment, which freezes the
canvas. Caught on the rig, not by reading.
No sketch open: unchanged — a new Sketch feature, still dropped on a picked face.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 6724ea27c5 (DesignPanel.cpp applied as a patch; parity 30, shared files
byte-identical).
chrome_only's rule is "acts on the DOCUMENT, not on a selection". Text and SVG both call
add_imported_sketch(), which drops the art on a picked solid face via
SketchPlane::from_face() — a selection-consuming profile creator, like Sketch. Now
sk_text / sk_svg in the sketch half's Create row, where their toolbar buttons already sit.
Verified on the rig: Create ends Point, Text, SVG.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca e5e223a794 (DesignPanel.cpp applied as a patch; parity 30, hpp
byte-identical).
Every body combo opened on index 0, so picking a body and pressing Mirror acted on a
different solid while the card showed that other body as the target. Nine sites now read
the viewport selection; Boolean takes the picked body as target and a different one as
tool, since defaulting both to the same body is a no-op.
Verified functionally on the rig: picked the 20x20 body, mirrored, and the new body
measures 20x20 — not the 80x50 one it would have used before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
* fixes: may be used uninitialized [-Wmaybe-uninitialized]
* fixes: arc_len_next may be used uninitialized [-Wmaybe-uninitialized]
* review result: reverts {} initializer with = to keep code style consistent
Mirror of snaporca e2ef3018cd. Data only, and only the `why` prose in two slots — the
generated DesignOffer.hpp is byte-identical, so there is nothing to rebuild.
Ratified 2026-08-01: the offer deliberately splits the toolbar's dressup family. Shell
hollows a solid so it sits in Remove; Delete Face edits an existing solid so it sits in
Modify. Recorded in both slots' `why` so either half explains the split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 352cf1c259 (data only — tool_atlas.json + the regenerated
DesignOffer.hpp; both byte-identical across the forks).
"Fillet / chamfer / draft" rather than naming shell too: Shell is in Remove and Delete
Face in Modify. Only the toolbar's dressup dropdown groups all five.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 3677937964 (DesignPanel.cpp applied as a patch; parity 30, shared
files byte-identical).
The card header read "Fillet 1" over a chamfer because the offer's Chamfer address opened
the tool before setting the type, and open_tool() titles the card from that combo. Choose
first, then open.
"Dress-up" removed from the offer row (-> "Fillet / chamfer"), the card field (-> "Type",
it was a label reading Dress-up whose value said Chamfer) and the toolbar tooltip.
Verified on the rig: header "Chamfer 1", field "Type: Chamfer", row "Fillet / chamfer".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 6d5510734b (DesignPanel.cpp applied as a patch; parity re-checked at
30 lines, shared files byte-identical).
Polygon's Sides/Circumscribed card is deleted — the choice is made in Create > Polygon,
which names the counts and the two fits, because the side count cannot be recovered after
drawing. Dress-up, Combine and Pattern were single verbs hiding several behind a combo
and now name each one in the offer. Fixes fillet and chamfer both carrying key:S+F, which
made the offer's Chamfer open a Fillet.
Built green and verified on the rig: the Dress-up card opened from Chamfer reads Chamfer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca 0c83f59f13 (DesignPanel.cpp applied as a patch, not copied, so this
fork's 30 permitted divergent lines survive; parity re-checked at 30/16 with the shared
files byte-identical).
The sketch dropdown never registered "fly:<family>#<row>" addresses the way
feat_dropdown does for model verbs, so the offer could name a family but only ever arm
its first tool — Rectangle always gave a corner rectangle. Adds the registration, 14
atlas verbs (including the entire array family, which was absent, and rotate/scale), an
action for the sk_move row that previously did nothing when picked, and a second submenu
level so variants nest under their family instead of flattening 19 create tools.
Built green and verified on the rig: Oblique rectangle arms oblique, not corner.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Mirror of snaporca ac85277bac..0e7cb3ec78 (six changes, applied as a patch to
DesignPanel.cpp rather than copied, so this fork's 30 permitted divergent lines survive
— parity re-checked afterwards: the five shared files are byte-identical, DesignCanvas.cpp
and DesignPanel.cpp differ by exactly 16 and 30 lines).
- The straight slot's inline field says Radius, which is what it sets. It stores the
half-width and passed the typed number through unchanged, so 30 produced a 60 mm slot.
- The offer opens from the keyboard (Menu, Shift+F10), anchored on the viewport rather
than wherever the pointer happens to be. The card hint names the new route.
- The sketch card's Plane combo is gone; the plane comes from the viewport. Also stops
build_candidate collapsing a face plane to a base plane while editing.
- Mass properties and the dead Edit row are wired into the offer; DesignOffer.hpp is
regenerated from tool_atlas.json, verified by re-running the generator and diffing.
- docs/rig_build_traps.md + scripts/rig-build.sh, which derives its fork identity from
project() so it cannot be pointed at the other fork's image or volume.
- docs/design_tab.md refreshed (44 commits stale) + a PR description, with this fork's
own merge-base and diff shape rather than snaporca's.
Built green in the deps container with the new script and verified on the rig: Menu and
Shift+F10 both open the offer at the viewport centre with the pointer parked off-canvas,
and the sketch card shows no Plane row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
LayerResult's second field is typed size_t, so std::numeric_limits::max
should also use size_t and not something related to coordinates for the
layer_id.
Confirming a sketch while an inline value field was open left the field behind. The
editor is a top-level frame, so it survived the session that owned it, and inline_busy
stayed set with it — on_mouse_impl then returned true at its first branch for every
later click and the viewport was simply dead. No refusal, no message: exactly the
"click the geometry, nothing happens" the pick bugs above it were mistaken for.
finish() and cancel() now call close_session_chrome(): dismiss the open field
(keep-as-drawn, the same contract the polyline terminators already use), drop the
queue of fields behind it, and clear the corner readout — which had the same defect
for the same reason, sitting on 336.8° over a committed sketch because nothing redraws
the HUD once the tool stops.
Verified on the rig on the exact reported sequence: line on XZ, Return to accept the
length, Confirm with the Angle field still open. The field goes, the sketch commits,
and the next click reaches the pick (pick trace shows down/up consumed) and selects
Sketch1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Type a new length into a sketch quote and the number changed while the geometry
sat still, with the solver dropping to "Conflicting constraints" — a broken
constraint state the user never asked for, arrived at by doing the one thing the
status line invited.
An experiment separated the two candidate causes. Same gesture, one variable:
a line committed WITH a driving length refused the edit and conflicted; a line
committed with none (Esc keeps it as drawn) accepted it and visibly shrank. So
the value was never the problem and the solver was not wrong — it was being
handed two contradicting constraints and correctly declining to choose.
The quote-edit path appended unconditionally:
a.con = int(m_constraints.size());
m_constraints.push_back(constraint_for(a));
Accepting the length at draw time creates Distance(P0,P1) = 64.9. Clicking the
quote later creates a SECOND Distance on the same two points asking for 30. Over
-constrained by construction. A line with no dimension yet only ever gets one
constraint, which is exactly why this looked intermittent rather than total.
upsert_constraint() finds an existing constraint with the same type and operands,
overwrites its value and returns its index; it appends only when there is none.
Operand order is ignored — a Distance from A to B is the same constraint as B to
A, and so is an Angle. Returning the index matters as much as the update: it
keeps the annotation's `con` pointing at the constraint that is actually live, so
the NEXT edit is an update too rather than reverting to appending after one good
round. upsert_dimension() applies the same rule to the visible quote, which had
been stacking labels reading different values on the same pixel, and keeps the
existing label position so a placed quote does not teleport.
set_dimension_value already did the right thing through a.con. The machinery
existed; these two call sites never consulted it.
Verified on :11 on the exact failing case — draw a line, Return to lock the
length, Confirm, double-click to re-open, click the line, type 30 into the quote:
the line shrinks, the quote reads 30.0 mm, one label not two, and the solver
stays at "3 degrees of freedom" with no conflict.
NOT included: record_dimension_constraint() has the same unconditional push_back
in all six of its branches. It belongs to the legacy Constrain mode with
different selection semantics and I could not exercise it, so it is flagged
rather than changed blind.
Refs snaporca-e1p.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Selecting a committed sketch line lit the right tree row and then told the user
to go and press Edit in the panel. That is the side-panel dependency this tab
exists to remove, and it made "selectable" true while "editable from the
geometry" stayed false.
on_edit_feature already does the whole job — re-open the entities in the sketch
UI with handles and live quotes — and was only ever reachable from a tree row.
A double-click on a committed stroke now calls it for that feature. Double-click
on empty space still fits the view, so nothing is taken away.
The stroke hit test is now one hit_display_sketch() shared by the click and the
double-click. Two copies of "what is under the pointer" drift, and a double-click
acting on a different entity than the click before it is a miserable thing to
chase. It also reports the entity index, which the tracer prints, so a pick that
lands on the wrong stroke can be seen rather than inferred.
Verified on :11 end to end: draw an open line, commit, double-click it. The
tracer prints "double-click -> edit sketch feature 0 (entity 0)", the panel
reads "Editing sketch — drag a handle or click a quote to edit", and a click
inside the session selects the line with endpoint handles, live quotes and
"1 selected — Delete removes them".
NOT delivered by this commit, found while verifying it: typing a new value into
a length quote is accepted and displayed but the geometry does not move and the
solver drops to "Conflicting constraints". Filed separately — it lives in the
constraint layer, not in selection, and nothing here touches it.
Refs snaporca-e1p.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Reported from the rig: a committed sketch holding a single open line rendered on
the plate and could not be selected, so it could not be edited or deleted from
the viewport at all.
The viewport pick for committed sketches iterated region_loops(). That function
exists to find EXTRUDABLE regions and, as its own walk comment says, an open
chain "stalls" and is discarded. So for a sketch of open entities it returns an
empty list, the pick loop has nothing to iterate, and every click falls through
to bare plate. Not a tolerance problem and not a focus problem: there was no
candidate geometry to test against.
Whether a stroke bounds a closed region has nothing to do with whether the user
can point at it. The stroke test now covers every non-construction entity, and a
separate entity->region map preserves what a hit REPORTS, so a click inside a
closed loop still names that loop exactly as before. Region membership decides
the report, not whether the hit can happen.
The rest of the path was already written and simply unreachable: the panel's
handler has a region < 0 branch that selects the feature, highlights it in the
tree and says "Sketch selected — Extrude it, or Edit / Delete from the tree".
This makes existing behaviour reachable rather than adding new behaviour.
Verified on :11 with the pick tracer (SNAPORCA_PICK_TRACE=1), which is what
distinguished the two failure modes: before, the click reached the handler and
fell through to handle_solid_click ("no solid data"); after, it is consumed by
the display-sketch test and never reaches it, and the panel reads "Sketch
selected" with Sketch1 lit in the tree.
Construction geometry stays unpickable, matching region_loops' own filter. It is
the same class of bug — a construction line cannot be selected to delete it —
but including it risks construction stealing picks from real geometry, so it is
left as a separate decision.
Refs snaporca-e1p.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
contradicting itself about the plane
Reported from the rig: pick a plane, press Sketch, and you are told to pick a
plane. The app prescribed a sequence and then refused to acknowledge that you
had followed it. Right-click did nothing, so there was no way to reach a
drawing tool except the toolbar this tab exists to retire.
act_sketch was two lines: set the mode, then print "Click a face or a reference
plane in the viewport, then a sketch tool" — unconditionally, without ever
asking whether a plane was already chosen. The plane was never lost; m_ref_plane
held it and begin_sketch captures it when the first tool is armed. The sentence
was simply false.
It now asks. sketch_plane_target() is a companion to sketch_plane_from_selection
that distinguishes "the user chose XZ" from "nothing chosen, falling back to XY"
— a distinction m_ref_plane cannot express on its own, being always a valid
index, so m_plane_picked carries it. With a target the readout names it and the
offer opens on the Create row; without one the old prompt stands, because then
it is true. The card above the status line was a local wxStaticText that nothing
could update, so it went on asking for a plane two inches from a line saying the
plane was chosen. It is a member now and the two are written together.
Right-click inside a sketch was excluded wholesale so that it could end a
polyline chain, abandon an anchor, exit a tool. That made every sketch row in
the atlas unreachable. The honest test is not which mode we are in but whether
the tool actually USED this right-click, and only the tool knows: on_mouse now
wraps on_mouse_impl and records that once, for every terminator, instead of
threading a flag through the twenty-odd sites that consume a RightDown. The
canvas read-and-clears it on the matching release.
Underneath all of it was one confusion — MODE versus SESSION — at four sites.
begin_sketch does not run until the first tool is armed, so is_sketching() is
false for exactly the interval between "press Sketch" and "pick a tool", which
is precisely when the drawing tools must be on offer. The keyboard learned this
once already (snaporca-0ud, whose comment states the rule) and I reintroduced it
in offer_selection_kind and again in show_offer_menu, where the offer built from
the FEATURE map and rendered nine rows that all refused the sketch selection.
Both now call sketch_map_applies(), so they cannot drift apart again. The
keyboard keeps its own split: its "sketching" gates undo and delete-last-entity,
which genuinely need a live session.
Verified on :11 against a fresh build. Pick XZ, press Sketch: card reads
"Drawing on XZ", status reads "Sketching on XZ — pick a tool", offer opens with
Create and Reference live and the six rows needing geometry greyed. Right-click
while idle opens the offer. Right-click as a terminator does NOT — the chain
ends, the line lands on XZ, its length field arms at 49.36 mm. That last one is
the regression the blanket exclusion was buying and the reason this shape of fix
was chosen over a mode test.
Refs snaporca-6vs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
The pick cycle died two commits ago and left no viewport route to a whole
body at all: one click resolves vertex, edge or face, double-click is
already zoom-to-fit, and the only way to take a body was the Bodies list —
a geometry-first violation for as long as it stood. The rubber band is that
route.
Left-drag is the gesture, as asked. That button was orbit, so this canvas
now maps the mouse the way every CAD the user already knows does: left
selects, middle orbits, right pans. The change is a single flag on
GLCanvas3D set only by DesignCanvas, so Prepare and Preview keep the mouse
their users learned. Sketch mode inherits the same mapping, which is the
consistent reading — Design is one modality, not two.
Past an 8 px budget a press becomes a sweep, anchored at the ORIGINAL press
point rather than at the frame where the threshold was crossed, so the
first few pixels are not lost. Below the budget it is still a click and the
existing vertex/edge/face pick runs untouched. Sampling is the display
mesh's triangle vertices plus centroids — the same points the ray pick
tests, already in world coordinates — and the body with the most samples
inside wins, because the selection callback downstream carries one body.
Crossing semantics: touching selects. Enclosed-only for left-to-right and
crossing for right-to-left is the fuller CAD convention and is deferred,
not forgotten; with one selectable body it would have bought nothing.
Two defects fixed on the way, both found by exercising this:
Right-drag pans, and every pan ended by popping the offer over wherever the
camera stopped — the context menu arriving as the reward for moving the
view. The offer is now the release of a STATIONARY right-click, at the same
8 px budget the pick uses.
The selection handler wrote m_status twice. Only the later write ever
reached the screen, so the earlier block had been dead since it was
written, and its labels drifted out of step with the live ones unnoticed —
including a vertex fix I made this morning in the branch that never
renders. Deleted, with a note saying why, rather than left as two writers
for the next person to pick the wrong one.
Verified on :11 against a fresh build: click takes face 5; left-drag across
the body reports "selected (whole body)" with the whole solid tinted and
the camera unmoved; left-drag over empty space clears; stationary
right-click opens the offer; right-drag pans with no menu; middle-drag
orbits. Precedence re-checked after the deletion — face at 25 px from the
corner, vertex from 10 px in.
Refs snaporca-9xw.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
Completes the precedence Tommaso asked for: vertex, then edge, then face, all
from ONE click, all decided in screen pixels. Verified on :11 by sweeping into
the right corner of a plate — x=1250 and 1290 report "face 5 selected", x=1308,
1318 and 1323 report "vertex selected" — and the cyan marker lands exactly on
the corner in the render.
The vertex tolerance (11 px) is deliberately LARGER than the edge one (8 px). A
corner lies ON its edges, so equal radii would make vertices unreachable: every
click near one would resolve to the edge underneath. Bigger-wins-first is what
makes the smallest entity actually pickable.
Vertices come from the sampled edge polylines' endpoints rather than a separate
topology walk — every corner of a face is the end of one of its edges, so the
data was already in hand.
The highlight is a camera-facing square scaled by 1/zoom, the same trick the
edge ribbon uses, so it reads as a constant dot at any zoom. This is why vertex
picking did not ship with the previous commit: the Edge render path billboards
a ribbon and degenerates on a two-point input, and a selection you cannot see
is not a selection (L5). Better to add the primitive than to fake the feature.
DesignPanel now distinguishes level 4: a picked corner sets neither face nor
edge, because a corner is not its face, and the offer classifies it as
OfferSel::Vertex — where Plane, Axis and Coord Sys already accept it.
snaporca-9xw (rubber band still open — see the issue).
Tommaso, correctly: fix selection before building on it. I had taken the
whole→face→edge click cycle as terrain and hung the tool offer off it, when §10
of the charter already listed that cycle as an L5 violation. An offer can only
ever be as truthful as the selection beneath it, so this is the foundation and
it should have come first.
NOW: one click selects the SMALLEST thing under the pointer — the edge if the
cursor is within tolerance of one, otherwise the face. No repeat clicks, no
state, no memory of what was picked before. Verified by sweeping a column of
single clicks down a plate on :11: y=800..915 all report "face 5 selected", and
y=925/935 — within a few pixels of the front edge — report "edge 3 selected".
One gesture, one deterministic result, which is what L5 asks for.
TOLERANCE IS IN SCREEN PIXELS. The old edge step compared a ray-to-segment
distance in millimetres, so the same gesture meant different things at
different zoom levels. The pointer is a screen object; its tolerance has to be
one too. 8 px, measured against the edge polyline projected through the camera.
WHAT IS NOT HERE, AND WHY IT IS NOT FAKED. Whole-body selection has no viewport
gesture in this commit. Double-click is ALREADY zoom-to-fit, bound earlier in
the same on_mouse, and this pick runs on LeftUp where LeftDClick() can never be
true — so a double-click branch here would have been dead code that reads like
a working feature. I wrote one, found it unreachable, and deleted it rather
than leave it. The body gesture is the rubber band, which is its own piece of
work; until it lands bodies are selected from the Bodies list, and the hole is
named in a comment at the site instead of being left for someone to trip over.
Six status strings that promised the cycle ("click again for a face", "click
again for an edge", "click again to reset") are gone — they described a
behaviour that no longer exists, and a hint that lies is worse than none.
Both forks build. Parity: DesignSketchTool.cpp byte-identical, DesignPanel.cpp
30 divergent lines — the invariant exactly.
snaporca-6vs.
# Description
On multi-tool printers using the type 2 wipe tower, the travel to the
tower ignored the configured Z hop type and always used a plain vertical
hop, so the nozzle rose in place over the part and oozed instead of
lifting away with the travel. It now follows the filament's Z hop
setting, matching what the type 1 tower already does.
Only toolchange travels to a type 2 tower change. Normal Lift and z_hop
= 0 are unaffected, and no extrusion moves change in any mode.
# Screenshots/Recordings/Graphs
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
Row order is RATIFIED (charter 4.1, 2026-07-31) and this is the first working
implementation of it: right-click in the Design viewport and a vertical list
opens at the pointer with the eight families in their fixed order, the verbs
that apply live, and the ones that do not disabled IN PLACE carrying their
reason.
THE MAP EXISTS ONCE. DesignOffer.hpp is GENERATED from docs/ux/tool_atlas.json
by docs/ux/mockups/gen_offer_table.py — the same file the 113 mockups are drawn
from. A drawing and the product therefore cannot drift apart, which is the only
way row constancy survives contact with a codebase. Never hand-edit the header.
NOTHING IS RE-IMPLEMENTED. Each row routes to the code that already runs that
verb: "key:S+E" through m_keys_feature, "key:L" through m_keys_sketch,
"fly:material#4" through the feature flyout's own action, "btn:colour" through
the standalone button. The offer is a second door onto the same room, so the
toolbar, the shortcuts and the menu cannot drift into three behaviours. The 8
verbs with kernel support but no GUI path show disabled, which is honest and
matches section 10 of the charter.
Right-click only fires the offer when the canvas is IDLE. Right-click already
ends a polyline chain and finishes the move gizmo; taking those over would
break two working interactions to add a third.
Two things the running build corrected, both found by looking at screenshots:
- THE REASON MUST BE TRUE FOR WHAT IS IN FRONT OF THE USER. Taking the first
refusal in a family printed "Transform needs a body — add or import one
first" on a document that HAS a body, because the real obstacle was that
nothing was selected. Now the reason comes from a verb that accepts the
current selection and fails only on document state; if no verb in the family
accepts this selection at all, it says "select something first" or says
nothing. A menu whose whole value is telling the truth cannot ship a lie.
- Classification follows the level the pick cycle has REACHED, not the face the
ray happened to hit, so the header cannot name a face while the whole body is
lit. Sketching on the face you merely clicked is untouched — that path is
sketch_plane_from_selection (snaporca-3a2).
Verified on :11 end to end: nothing selected shows Sketch live with Shift+S and
seven greyed rows each explaining itself; a selected solid shows Move directly
with Shift+Y (one applicable verb, so no submenu and no extra click) and five
families as submenus. Both forks compile and link.
Fork parity re-checked after the port: DesignPanel.cpp 30 divergent lines,
DesignCanvas.cpp 16, every other CAD file byte-identical — the invariant exactly.
snaporca-96r.
The tower travel took retract()'s default vertical Z hop instead of the
configured one, so the nozzle rose in place over the part and oozed
rather than departing with the travel. Pass the filament's z_hop_types
through, mapping Auto to a spiral lift as append_tcr does.
* Sync WipeTower from BambuStudio(through ca1881761)
* Fix post-slice self-invalidation on custom multi-extruder printers
* Complete the rib wipe tower port in WipeTower2
The rib tower is now always square (prime_tower_width is ignored, as the
GUI already implies), carries the rib origin offset like the BBL tower so
the rib tips sit inside the configured position, clamps the rib length to
the tower diagonal, and extends the ribs for short towers.
* Use the squared rib tower size in arrange estimates
estimate_wipe_tower_polygon reserved the arrange footprint and clamped the
tower X position with the raw prime_tower_width, under-reserving space
whenever the rib wall squares the tower to a different width.
* Print the WipeTower2 shell with a non-support, non-soluble filament
Like the BBL tower: the layer's sparse infill, wall, and brim go to the
first toolchange to a non-support/non-soluble filament, or are printed
with the incoming filament before any toolchange. The minimal-purge
clamp now also covers toolchanges that get no finish-layer saving.
Output is unchanged when no support/soluble filament is used.
* Port the skip-points gap wall to WipeTower2
prime_tower_skip_points was stubbed for Type2 towers: the wall call
hard-coded skip_points=false, the gap cutter received an empty vector,
and append_tcr2 never routed the entry travel. Now the toolchange entry
positions are precomputed from the finalized plan, the wall is cut open
at each entry, and the entry travel approaches around the tower bounding
box through the opening when it starts outside the tower. The geometry
helpers are re-synced with the BBL versions (add_extra_point guards,
per-point side selection). The cone wall keeps its separate path, where
the option stays inert.
Behavior change: non-BBL towers now honor the (default-on) checkbox with
gap walls and routed entries; with the option off the output is
unchanged, and the BBL tower path is untouched.
* Route the in-place toolchange tower entry through the skip-point gap
On multi-tool printers without ramming the tool changes away from the
tower and the entry travel is the tcr's own positioning move, which went
straight across the printed wall. Append the avoid-perimeter path to the
change-filament gcode instead, so the head approaches around the tower
and enters through the wall opening (append_tcr parity).
* Iron the purge start out through the skip-point gap in WipeTower2
Port the BBL tower's entry line ironing: extrude the first 3 mm of the
purge, retract, drag the nozzle 1.5x back out through the wall gap at
F600, creep back at F240 and unretract, so the toolchange start blob
ends up in the gap instead of on the wall. Fires only when the purge
starts at the left-edge entry heading right (in-place toolchangers);
SEMM ram/cooling wipes start mid-box and the priming line has no wall,
so both keep their previous output.
* Reserve WipeTower2 toolchange depth to match the printed purge
The planner reserved ramming rows gated only on enable_filament_ramming and
sized them with the SEMM 0.25s time step, while toolchange_Unload rams on
(semm && enable_filament_ramming) || filament_multitool_ramming with the
multitool time step. Disabling multitool ramming therefore left ~3 unprinted
rows per toolchange as blank bands in the tower. Without ramming the first
wipe line also needs reserved depth of its own (it no longer rides the last
ramming row), plus the y_step/2 offset the wipe start inherits from the
ramming start position - otherwise the tightened boxes truncate the ordered
purge at the box edge.
* Tile WipeTower2 purge rows contiguously across toolchange blocks
Without ramming, each purge block reserved one wipe pitch more than its
rows occupy (ceil+1 rounding plus the ram-geometry start offset), and the
wipe began a full pitch inside the block, leaving a blank band of exactly
two pitches between adjacent blocks. Plan the block as whole wipe rows,
start the first row so the row lattice continues across the block
boundary, and fill the reserved box instead of stopping at the ordered
volume, mirroring how the BBL WipeTower keeps planned depth identical to
printed rows. Ram-printing toolchanges (SEMM with ramming enabled,
multitool ramming) are unchanged.
* Scrub the WipeTower2 toolchange entry with the BBL flat-ironing spiral
The entry scrub now matches the BBL tower's toolchange_wipe_new sequence:
after the ironing drag the retracted nozzle runs a dry expanding-square
spiral centred on the wall-gap entry point before resuming the purge row.
The spiral runs whenever the gap wall is on (disable per filament via
filament_tower_ironing_area = 0); WipeTower2 no longer reads
prime_tower_flat_ironing.
* Restart the WipeTower2 wipe at the box boundary after multitool ramming
With the gap wall on a multi-tool printer, quantize the ram band up to its
whole reserved rows (as the BBL tower does for the old-tool purge) and start
CP TOOLCHANGE WIPE at the left-edge boundary on a fresh row below it instead
of continuing from wherever the ram serpentine ended. The entry scrub then
runs at the wall gap on ram toolchanges too, and the wipe box is whole rows,
so it is filled completely like the no-ram case. SEMM and skip-points-off
behavior is unchanged.
* Move the WipeTower2 wall gap to the wipe start row for ram toolchanges
* code cleanup
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix typo
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Folds the form-factor decision into the charter. 4.1 is rewritten around
Tommaso's proposal — left-click selects, right-click opens a vertical list of
icon / name / shortcut — and the radial is demoted to a "Rejected" subsection
rather than deleted, because it is a good idea that loses on evidence and
somebody will propose it again. The evidence is recorded with it: mean fill of
3.45 of 8, only two live slots on a fresh document, sketch Create needing nine
addresses on an eight-slot ring, names that do not fit around a circle in
translation, and a 380px disc over the model on a 1366x768 screen.
The invariant survives the change of geometry, which is the useful proof: same
eight families, same fixed order, nothing re-sorted or compacted. It is now
stated as ROW constancy, and the one substantive gain is that unavailable verbs
are disabled IN PLACE carrying their own reason, in strings the product already
ships. An empty ring slot was mute; a greyed row teaches. On a first-run
document the offer stops being a mostly-blank control and becomes a map of what
the product does and what you must do first — which is the section 2 audience
in one picture.
Opening it is now a table rather than an assumption, because "right-click" is
not a universal gesture: two-button mouse right-clicks, trackpads two-finger
tap, a one-button Mac LONG-PRESSES or Ctrl-clicks, touch and pen long-press,
and the keyboard uses the Menu key or Shift+F10. The long-press is explicitly
an ADDITIONAL route — 6.2 forbids press-and-hold as a sole path and that
stands, so the rule now names its own exception and closes it — and it must
show that it is charging, or a user who lets go early concludes the product is
broken (L5).
Consequently: 6.2's keyboard bullet describes opening and walking the offer by
key rather than by compass direction; the gate gains question 13 (every new
pointer gesture declares its keyboard equivalent and what a one-button Mac, a
trackpad and a touch screen do) and question 12 now asks whether tool_atlas.json
was updated and the atlas regenerated; section 10 points at the rendered atlas
and says the outstanding thing is ratifying row order, not drawing the map.
snaporca-96r.
Tommaso was not sure about the ring and proposed a vertical list: left-click
selects, right-click exposes icon / name / shortcut. Drawn, it is better, and
the reasons are visible in the renders rather than arguable.
THE DISABLED ROW CAN SPEAK. This is the one that decides it. A ring slot that
does not apply is an empty circle: it says nothing, and on a fresh document six
of the eight are empty. A list row that does not apply is greyed IN PLACE with
its own name and its own reason — "Create a sketch, or pick a solid face,
first", "Create a solid body to pattern first" — which are strings the product
already ships and which tool_atlas.json already carries. The first-run picture
stops being a mostly-empty ring and becomes a map of what the product does and
what you must do first. For the audience section 2 puts first, that is the
whole ballgame.
THE OVERFLOW DISAPPEARS. Sketch Create needs nine addresses; a ring of eight
pushed Polygon and Point behind a "More" slot. Nine rows is just nine rows. The
one measured defect in the ring design is not a defect in this one.
SHORTCUTS READ AS A COLUMN. Right-aligned in a list they stack into something
the eye learns passively, which is exactly the graduation path 4.1 claims —
and it is the mechanism by which the power user Tommaso describes stops opening
the menu at all. Around a ring the same keys are eight loose chips.
Also, unglamorously: long translated names fit, arrow keys and screen readers
work natively where a radial needs special handling, and a 324px box costs the
1366x768 machine far less than a 380px disc over the model.
What the ring keeps: equidistant targets and a future flick gesture. Since the
brief is that power users live on the keyboard, that buys less than it looks.
The invariant is untouched — same eight families, same fixed order, nothing
re-sorted, nothing compacted. Only the geometry changed, which is the point:
the map survived a change of form factor, so it was a real map.
Both forms are now rendered side by side for the same states, and the atlas
opens with the pairs.
snaporca-96r.
The charter fixed the slot-constancy invariant but carried one hand-written
eight-cell table as an illustration, and nothing of the offer exists in the
product. Before any GUI code, the group needs the map itself: what verbs there
are, what each one needs before it can be offered, and what the ring actually
looks like in every situation a user can put it in.
tool_atlas.json is the source of truth and it was extracted from the code, not
from memory: verbs, shortcuts and the exact refusal strings from DesignPanel's
six feat_dropdown call sites and the sk_key table, kernel coverage checked
against CadFeatureType, headless coverage against McpControl's dispatch, and
the selection kinds taken from the callbacks DesignCanvas actually exposes. It
carries each verb's preconditions, so "why is that slot empty" has an answer
already written in the product's own words. The eventual C++ table generates
from this file too — the map exists once.
gen_offer_mockups.py renders it: 20 selection kinds x 2 document states = 40
primary rings, plus 73 sub-rings, plus comparison sheets. 113 states, none of
them hand-drawn, because a human drawing 113 rings is exactly how an address
quietly changes. Everything is framed at 1366x768, the charter's own reach
target, so L11 is tested in the mockups before it is tested in code.
Three things the drawing found that the prose had not:
- MEAN FILL IS 3.45 OF 8. The empty-slot rule is cheap in argument and
expensive on screen; on a fresh document exactly two slots are live. That
picture is the anti-clutter thesis made literal and it is the strongest image
in the set.
- SUB-RINGS MUST ANCHOR ON THEIR PARENT. Fanning them from north put Extrude at
N, which is Create's address in the primary map, so the second level
contradicted the first. Anchored, an address is two consistent strokes: Add
material is NE and its first verb is NE again.
- ONE FAMILY OVERFLOWS, AND ONLY ONE. Sketch-mode Create needs nine addresses
on an eight-slot ring. That is the ninth-position pressure the charter
predicted, arriving on schedule and measured rather than argued: either Point
moves family, or the tail goes to a third level, or the ring is not eight.
Every model-mode family fits. The generator refuses to wrap a tenth verb onto
a first — silent collision is the one outcome worse than an ugly ring — and
reports the overflow instead.
Also fixed while looking at renders: the selection pill sat on top of the north
slot's shortcut chip and hid it, and the scrim at 0.55 swallowed the very face
the ring had been opened on, which is 4.1 failing inside its own mockup.
Fork-neutral: the generator and everything it emits name no product, so both
forks carry byte-identical copies.
snaporca-2is.
Tommaso's requirement, and it changes what the offer IS: a tool must sit in the
same physical position whatever you selected. Click a face, an edge or a text
and fillet is in fillet's place every time. Position becomes an address the hand
learns, and the eye stops being needed.
That kills the ordering rule this section had two commits ago. "Most-used first
for that kind of selection" is adaptive ordering, and adaptive ordering destroys
the one property that makes a spatial menu fast — worse, it destroys it exactly
for the user who has just started to learn the layout. Office 2000 shipped that
idea and withdrew it. So: NO adaptive ordering, ever, in any form.
The invariant, written to survive every future feature: every tool has exactly
one address; that address is identical in every selection type where the tool
appears; slots for inapplicable tools are left EMPTY rather than compacted; and
adding a tool never re-addresses an existing one. Empty slots are the price of
constancy and they are cheap — a compacted offer is denser and unlearnable, a
sparse one is memorised in a week. An empty slot also answers a question ("this
cannot be done to this thing") that a silently-inert tool does not.
Radial rather than a strip, reversing what I proposed last time and for a reason
that only appears once constancy is the requirement: a direction from the click
point is an absolute address that survives the offer opening anywhere on screen
and survives being clamped at a screen edge, while "third item down" does not.
Centre is a hole so the picked geometry stays visible, and it names what is
selected, so a mis-pick is caught before a verb is chosen.
Every slot carries its keyboard shortcut beside the icon and the word. This is
the graduation path and it is why power users never see a conflict: you reach
for the place, the place says "F", and one day your hand types F before the ring
finishes drawing. The offer is the mechanism by which a beginner stops needing
the offer — one interface at two speeds, no advanced mode in between.
Also here: a proposed eight-position compass map across face/edge/body/text
(create, add, remove, dress-up, repeat, transform, reference, modify) offered as
the group's first ratification, with families opening a secondary ring under the
same rule; arrow/numpad direction addressing so the spatial map works from the
keyboard; a gate question 12 that treats re-addressing an existing tool as a
breaking change to every user's muscle memory.
snaporca-2is.
# 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?
-->
This PR updates OTA vendor profile handling and fixes a Windows-only
startup race that could terminate OrcaSlicer silently.
## Changes
- Correct vendor profile version comparison logic.
- Trigger vendor profile synchronization after the startup printer
preset is restored.
- Remove duplicate vendor synchronization from the general startup
updater.
- Replace global temporary archive cleanup with targeted per-vendor
cleanup.
- Add updater-thread exception handling to prevent uncaught filesystem
errors from terminating the process.
# 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.
-->
<!--
> 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)
Two changes to section 4, both from Tommaso.
FIRST: the selection does not merely feed the tool, it DETERMINES WHICH TOOLS
EXIST. Point at a planar face and the product offers the small set of things a
planar face can become; point at an edge and it offers fillet, chamfer and the
sketch tools that can reference it. Nothing else, because nothing else is
possible. This is the largest single thing available to us for a first-time
user, and the reason is worth writing down: a beginner's difficulty is not
operating a tool, it is not knowing which tools apply to what they are looking
at. Sixty icons answer a question they cannot yet ask; a face that offers its
own five verbs teaches the product by being used. It also deletes a whole class
of failure — a tool that silently does nothing because the selection was wrong
becomes unreachable.
The offer is an accelerator, not a toll gate: toolbar and single-letter
shortcuts keep working unchanged and consume the same selection, so an expert
never looks at the offer and a beginner never needs the toolbar. Both routes
land in the same place, which is how one interface serves all three audiences.
SECOND: "click empty space to commit" is withdrawn. It was an invisible gesture
with a destructive meaning — nothing on screen said it, and a stray click
committed a feature still being adjusted. Exactly what L5 forbids. A pending
feature now carries a confirm/cancel puck attached to its own geometry, beside
its handles, with Enter/Escape mirroring it; empty space reverts to the safe
meaning, clear the selection. The puck is an object in the scene, not a dialog:
the camera orbits, the values stay editable, nothing is blocked (L4 intact).
Two cases the rule has to get right or it damages the inner loop: continuous
tools (line, rectangle, circle) still commit each entity on its own gesture — a
tick per line would be miserable — and Enter/Escape end the tool rather than
confirm an entity. And ambiguity resolves toward keeping work: starting another
operation with a valid feature pending commits it rather than discarding it,
because undo reaches everything and the recoverable direction is the right
default.
Section 10 gains the two honest consequences: today a selection offers nothing
(the largest single item of new work this charter asks for) and committing is
still the invisible empty-space click.
snaporca-2is.
The charter had accessibility only in the assistive sense — keyboard, contrast,
colour, targets — and said nothing about who can get through the door in the
first place. That was the larger omission. The premise of an OSS CAD tool is
that a kid on a school laptop, with no licence, no account, no fast machine and
nobody to teach them, can open it and make a real thing; a tool that only the
equipped can run reaches people who were already going to design something.
So the fourteen-year-old is now the FIRST of three audiences, ahead of the maker
and the mechanical designer, with an explicit rule that when audiences conflict
the earlier one wins unless someone writes down why not.
L11 states the floor: runs completely on a low-end laptop with integrated
graphics at 1366x768, offline, no account, and no capability withheld behind a
tier, a plugin or a cloud service. Section 6 splits into 6.1 reach and 6.2 the
assistive floor: the reference machine, the small screen as the layout target
rather than the stretch case, files that belong to the user, learnable with no
documentation, plain language at the entry tier, and exploration that is never
punished — undo reaches everything, nothing asks the user to be sure.
Consequences elsewhere: the screen budget is set by the smallest screen we
serve, not the reviewer's monitor; the PR gate gains a reach question; B6 joins
the benchmark (the inner loop on the reference machine, offline, fresh install)
and every task is measured there rather than on a workstation. The side-panel
debt now fails L11 as well as L1 — on that screen the cards leave the model a
strip.
One role addition: the absent audience needs a seat. The kid cannot file an
issue, so someone owns B5/B6 and the group watches real first-timers quarterly.
Approachability is the one thing here that cannot be argued from principle.
snaporca-2is.
The doctrine is shared but the document is not: each fork's copy now speaks
about its own product only, so it reads as that project's own charter rather
than as a note about a sibling repository. This is a deliberate divergence —
the two copies must NOT be reconciled by a parity sweep. The CAD sources stay
byte-identical; only this doc branches.
snaporca-2is.
The call with SoftFever settled that Orca-CAD is one of the branches to be
implemented and that a design+dev group forms around it. A group without a
written doctrine reviews by taste, and a CAD reviewed by taste becomes
FreeCAD one locally-reasonable side panel at a time.
So the doctrine is written first, as something a reviewer can FAIL a pull
request against: ten laws each with its own test, the interaction grammar
they compose into, the accessibility floor as a merge requirement, and a
ten-question gate answered in every UI pull request.
The position is Shapr3D's interaction economy, not its feature list —
direct, gestural, almost no chrome, depth revealed by what you touch. Depth
for mechanical designers arrives as progressive disclosure of tools that
never move, in three tiers, non-modal, with assemblies and exploded views
obeying the same point-then-act grammar as a beginner's extrude.
The one thing neither Shapr3D nor FreeCAD has is that we live inside a
slicer: plate, nozzle, material and build volume are known at design time,
so print-domain failures are warnings on the geometry, not a report.
Section 10 is an honest inventory: what already complies, and the six
things that violate the laws today, none of them defended. The appendix
keeps the anti-patterns we have already paid for, because each one is
cheap to reintroduce.
snaporca-2is.
The relaunch sequence was an ad-hoc pile of docker exec one-liners, and it
had a real bug: it dismissed the first-run dialogs by computing the
titlebar close box from `xdotool getwindowgeometry --shell` and clicking
it. When the dialog had already closed, that eval left the geometry
variables stale or empty, the click landed at a garbage coordinate, and it
kept hitting the Sketch button in the toolbar underneath — so the app came
up in sketch mode with a stray Sketch feature that then had to be
cancelled by hand. Three times in one session.
The fix is not a different mechanism. `xdotool windowclose` looks cleaner
and KILLS THE APP: it destroys the GdkWindow out from under the dialog and
the process dies with "GdkWindow unexpectedly destroyed", three
GLib-GObject criticals and a segfault. Measured, not guessed — that is
what the first version of this script did. Escape does not close the Setup
Wizard either, which is why it needs handling at all. So the titlebar
click stays, and what changes is that it refuses to click geometry it has
not validated: the window id is re-resolved immediately before, all four
geometry variables are unset first and must come back numeric, and the
computed point must be inside the screen. Any of those failing logs why
and clicks nothing.
Two further honesty fixes in the status output, both caught by reading it
rather than by it failing: app_pid skipped nothing, so with a container
full of <defunct> instances it printed a dead pid as though the session
were healthy — it now walks /proc/<pid>/stat and ignores zombies. And a
container without x11vnc reported "vnc: DOWN" as if something had broken,
when nothing was ever installed; it now says so, and does not try to start
what is not there.
Also replaces the fixed post-launch sleep with a wait for the main window,
because cold starts under software GL vary by a lot, and adds --status for
diagnosis: "no windows but the desktop is up" means the app died, "cannot
connect at all" means the desktop did. That distinction cost real time to
work out by hand.
Verified on both containers: one run each, wizard closed on validated
geometry, no stray sketch mode, live pid reported, and the app still up.
snaporca-e1p adjacent (tooling, not the tab itself).
The plane combo is gone. A sketch takes its plane from what is picked in
the viewport: a face on a solid, or one of the reference-plane ghosts,
clicked in 3D. The card is now a single line of instruction instead of a
control.
The combo had become worse than redundant. Once a picked face could be
the plane it displayed a row that CONTRADICTED the actual target — it
still said XY while the sketch went onto the face — so the one place a
user could look to confirm where they were drawing was the one place
guaranteed to be wrong.
What replaces it is state, not UI: m_ref_plane records which reference
plane was last clicked in 3D (0/1/2 = XY/XZ/YZ, >=3 indexes the datums)
and ref_plane_name() turns it into text for the on-geometry hint. Clicking
a ghost plane while a session is live re-planes it immediately, which the
combo's own handler used to do; that behaviour is kept, just driven from
the geometry instead of the widget. A plane click also drops a stale face
pick, so last pick wins in both directions.
populate_plane_choices() stays — seven other pickers use it (Plane base,
Axis A/B, Helix, Project, Mirror, Cut). Those are the next candidates,
tracked on snaporca-e1p; this commit only removes the one that had become
actively misleading.
Also: tessellation now matches Orca's OWN STEP importer, linear deflection
0.003 instead of 0.01 (Format/STEP.hpp default; angular was already 0.5
rad and unchanged). The Design viewport was never using a different
rendering technique — it hosts a real GLCanvas3D, builds a real
Model/ModelVolume and goes through the same reload/GLVolume path and the
same shaders as Prepare and Preview. What differed was the mesh handed to
it: 3.3x coarser than anything else in the application, which is why a
curved face read as faceted beside an imported part. Suite unaffected at
154 cases / 2125 assertions, so nothing depended on the old density.
Verified on :10: the card shows no dropdown, one click on a face then a
sketch tool still reports "on the picked face", and the circle is drawn in
that face's plane (artifacts/shots/h3a2-02-sketch.png, h3a2-03-drawn.png).
snaporca-e1p, snaporca-3a2.
The previous commit made a picked face the sketch plane and I verified it
by clicking the face TWICE. That was the wrong test. handle_solid_click
cycles whole -> face -> edge, and "First click on a (new) body/face
selects the WHOLE solid; refine on repeat clicks" — so at level 1
m_sel_solid_face is -1, and one click on a face, which is what selecting
a face means to anyone, still fell through to the plane combo. The fix
was real and unreachable, which from the outside is indistinguishable
from no fix at all.
The face id was never missing. handle_solid_click resolves it by ray on
the FIRST click and passes it to on_solid_selection_changed regardless of
the cycle level; the panel simply discarded it whenever level < 2. Keep
it in m_pick_face/m_pick_face_body and let a sketch use it, preferring an
explicit face-level selection when there is one. Nothing about the cycle
changes, so body operations that rely on whole-body selection are
untouched.
The new state is dropped wherever the existing picks are, so a stale face
cannot come back: choosing a body from the Bodies list (an explicit
choice with nothing pointed at), picking a committed sketch loop (last
pick wins), undo/redo (recompute invalidates topology ids), and when a
sketch consumes the face.
Verified on :10 with ONE click, which is the flow that was broken: build
a box, single-click its top face, S then C, and the hint reads "Circle —
click center, then radius · on the picked face" with the circle drawn in
that face's plane (artifacts/shots/g3a2-01-one-click.png,
g3a2-02-sketch.png, g3a2-03-drawn.png).
GUI-only, so the kernel suite is unaffected — plane_of_face and its 154
cases / 2125 assertions are unchanged from the previous commit.
snaporca-3a2.
Selecting a face and sketching on it is the most common gesture in solid
modelling, and it was impossible. The plane came from a combo holding
XY/XZ/YZ plus datums, and plane_from_choice had no face branch at all, so
the only route onto a face was to build a Coincident datum plane on it
first, confirm that, reopen the sketch and find the datum in the
dropdown. Three extra steps and a junk feature in the tree.
The fix is not another combo row. A new sketch now takes its plane from
what is SELECTED IN THE VIEWPORT: a picked planar face wins outright, and
only when nothing is picked does it fall back to the reference plane —
which is itself normally set by clicking one of the ghost planes in 3D,
not by opening the combo. The tool hint names the target ("Circle — click
center, then radius · on the picked face") so the choice is visible on the
geometry side rather than needing a control to read back.
CadDocument::plane_of_face is the shared derivation, so the sketch path
and the Coincident datum method cannot drift apart. It refuses
non-planar faces: face_normal_world evaluates at the mid parameter, which
on a cylinder or a fillet is a tangent plane at one arbitrary point —
fine for offsetting a datum, wrong as a sketch plane, and silently
sketching on a tangent is worse than declining.
Picking the face also CONSUMES it. Leaving the pick live meant the next
Extrude saw a selected face and push/pulled it instead of extruding the
sketch just drawn — the same trap the imported-art path already guards
against.
Verified on :10 end to end with no combo interaction: build a box, click
its top face twice to cycle whole -> face, press S then C, and the circle
is drawn in the plane of that face with its Radius tab on the geometry
(artifacts/shots/f3a2-03-face.png, f3a2-04-sketch-on-face.png,
f3a2-05-circle-drawn.png). Kernel side: 154 cases / 2125 assertions green
on both forks, including that a cylinder resolves exactly its two flat
caps and refuses the barrel.
Still side-panel-shaped and to be dealt with separately: the Plane combo
remains on the card and now merely displays a stale row when a face is
the real target. It should show the actual target or go away.
snaporca-3a2.
entities_to_wire handles exactly two shapes: one lone closed entity
(Circle/Ellipse), or a chain of open ones (Line/Arc/EllipseArc/BSpline).
Anything else -- a circle coexisting with a line, two circles -- returns a
null wire. build_sketch_wire answered that by falling through to its
legacy tail, which ends in a rectangle built from width/height. For an
entity sketch those fields are whatever they were initialised to, so the
extrude produced a box the user never drew, silently and with ok:true.
That is how the ellipse+stray-arc case in the P2 Tier-B.1 verification
turned into a default-rectangle solid.
Throw there instead. The legacy profile/shape paths below are still
reached by sketches that legitimately carry no entities at all, so the
enum and profile constructors are untouched -- only the case where
entities exist and cannot be turned into a wire now fails, which is
exactly the case that was fabricating geometry.
This does NOT implement the multi-loop support the issue asks for. Doing
that properly means deciding containment -- a circle inside a rectangle is
a hole, a circle beside it is a second region -- and make_extrude_regions
cannot be reused because it takes flattened Vec2d contours for imported
Text/SVG art and would discard the analytic circle. Guessing containment
would trade a visible failure for a wrong solid, which is the opposite of
the point. Left scoped on snaporca-88v.
Also converts the three float comparisons in the two test cases added this
session from Approx to WithinAbs/WithinRel, per tests/CLAUDE.md, which
rules Approx out for being asymmetric and double-only. The rest of the
file's pre-existing Approx uses are left alone.
153 cases / 2090 assertions green on both forks; no existing test depended
on the default-rectangle fallback.
snaporca-88v (partial: the silent-fallback half).
# Description
Python plugins currently have no way to localize their own dialogs to
match the app: the UI language is stored in `OrcaSlicer.conf`, which the
plugin audit hook deny-lists by design (the file sits next to cloud
secrets), and the host API exposes no app info. As a result, localized
plugins have to guess the language from the OS locale, which does not
always match the slicer UI (and the embedded interpreter often gets no
`LANG` at all in GUI sessions).
This PR adds a minimal read-only accessor:
```python
orca.host.app_language() # -> "en_US", "ru_RU", ...
```
It returns `GUI_App::current_language_code_safe()` — only the language
code string, nothing else from the config, so the audit-hook security
model is untouched.
No breaking changes; one file, +10 lines.
# Screenshots/Recordings/Graphs
(screenshots of the test dialog will be attached below)
## Tests
- Built on macOS (arm64, Ninja) with this change — compiles clean.
- Ran a minimal script-capability plugin calling
`orca.host.app_language()` on a system with Russian UI: the dialog shows
`'ru_RU'`.
- Guard before GUI init follows the same exception pattern as the
neighbouring `plater()`/`preset_bundle()` accessors.
<img width="903" height="826" alt="Снимок экрана 2026-07-28 в 23 10 48"
src="https://github.com/user-attachments/assets/27088265-b55d-4958-8602-7c3ab4993003"
/>
<img width="437" height="276" alt="Снимок экрана 2026-07-28 в 23 10 56"
src="https://github.com/user-attachments/assets/9845b401-dc8e-4353-aa24-5ace678270d6"
/>
With the gap wall on a multi-tool printer, quantize the ram band up to its
whole reserved rows (as the BBL tower does for the old-tool purge) and start
CP TOOLCHANGE WIPE at the left-edge boundary on a fresh row below it instead
of continuing from wherever the ram serpentine ended. The entry scrub then
runs at the wall gap on ram toolchanges too, and the wipe box is whole rows,
so it is filled completely like the no-ram case. SEMM and skip-points-off
behavior is unchanged.
The entry scrub now matches the BBL tower's toolchange_wipe_new sequence:
after the ironing drag the retracted nozzle runs a dry expanding-square
spiral centred on the wall-gap entry point before resuming the purge row.
The spiral runs whenever the gap wall is on (disable per filament via
filament_tower_ironing_area = 0); WipeTower2 no longer reads
prime_tower_flat_ironing.
Without ramming, each purge block reserved one wipe pitch more than its
rows occupy (ceil+1 rounding plus the ram-geometry start offset), and the
wipe began a full pitch inside the block, leaving a blank band of exactly
two pitches between adjacent blocks. Plan the block as whole wipe rows,
start the first row so the row lattice continues across the block
boundary, and fill the reserved box instead of stopping at the ordered
volume, mirroring how the BBL WipeTower keeps planned depth identical to
printed rows. Ram-printing toolchanges (SEMM with ramming enabled,
multitool ramming) are unchanged.
The planner reserved ramming rows gated only on enable_filament_ramming and
sized them with the SEMM 0.25s time step, while toolchange_Unload rams on
(semm && enable_filament_ramming) || filament_multitool_ramming with the
multitool time step. Disabling multitool ramming therefore left ~3 unprinted
rows per toolchange as blank bands in the tower. Without ramming the first
wipe line also needs reserved depth of its own (it no longer rides the last
ramming row), plus the y_step/2 offset the wipe start inherits from the
ramming start position - otherwise the tightened boxes truncate the ordered
purge at the box edge.
Plugins have no way to localize their own dialogs: the UI language lives in
OrcaSlicer.conf, which the plugin audit hook deny-lists because the file sits
next to cloud secrets. Add a read-only host accessor that returns just the
language code (current_language_code_safe), so plugins can match the app
language without touching the config file.
Port the BBL tower's entry line ironing: extrude the first 3 mm of the
purge, retract, drag the nozzle 1.5x back out through the wall gap at
F600, creep back at F240 and unretract, so the toolchange start blob
ends up in the gap instead of on the wall. Fires only when the purge
starts at the left-edge entry heading right (in-place toolchangers);
SEMM ram/cooling wipes start mid-box and the priming line has no wall,
so both keep their previous output.
* For dialog without explicitly `SetMinSize`, we should use `SetSizerAndFit` instead, otherwise the dialog will not show correctly on GTK3. (OrcaSlicer/OrcaSlicer#14561)
- and if `SetSizer` is called before the full layout has been built, then an extra `SetSizeHints` should be called before layout/fit so the min size can be properly set automatically based on children's min sizes accordingly.
* Fix GTK3 dialog min size: SetSizer → SetSizerAndFit for dialogs without explicit SetMinSize
Replace SetSizer() with SetSizerAndFit() in 11 dialog constructors that
neither call SetMinSize() nor SetSizeHints(), ensuring proper minimum
size propagation from child widgets on GTK3.
SetSizerAndFit internally calls sizer->SetSizeHints(window), which
sets the window's minimum size based on children — the same fix
applied to ProjectDropDialog in 8a7662083e.
Also drop sizer->Fit(this) calls where present, since they only
resize but don't set the min size hint needed by GTK3.
Co-Authored-By: Claude <noreply@anthropic.com>
* Update code style
* Update TroubleshootDialog.hpp
* Fix unsaved preset dialog layout
* Fix MsgDialog layout
* Fix other 3 instances in MsgDialog.cpp
* Fix a few more instances
* Fix printer option dialog too big on Windows
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
On multi-tool printers without ramming the tool changes away from the
tower and the entry travel is the tcr's own positioning move, which went
straight across the printed wall. Append the avoid-perimeter path to the
change-filament gcode instead, so the head approaches around the tower
and enters through the wall opening (append_tcr parity).
prime_tower_skip_points was stubbed for Type2 towers: the wall call
hard-coded skip_points=false, the gap cutter received an empty vector,
and append_tcr2 never routed the entry travel. Now the toolchange entry
positions are precomputed from the finalized plan, the wall is cut open
at each entry, and the entry travel approaches around the tower bounding
box through the opening when it starts outside the tower. The geometry
helpers are re-synced with the BBL versions (add_extra_point guards,
per-point side selection). The cone wall keeps its separate path, where
the option stays inert.
Behavior change: non-BBL towers now honor the (default-on) checkbox with
gap walls and routed entries; with the option off the output is
unchanged, and the BBL tower path is untouched.
A boolean cut whose tool misses the target is a perfectly legal
operation: OCCT reports IsDone(), the shape comes back unchanged, and the
feature lands in the recipe reporting ok:true. Driving the MCP socket,
that produced two consecutive {ok: true, bodies: 1, error: ''} responses
for a hole that was never drilled -- same viewport, same 46939.11 mm3 --
and the tree grew two Hole features that will never cut anything. A
caller, an agent especially, has no signal at all that the thing it asked
for did not happen.
Measure the volume across the op in route_feature's in-place branch and
refuse the no-op. Only for removals: Hole, Thread, and Extrude / Revolve
/ Sweep / Loft in Cut mode. Everything else may legitimately leave the
volume alone -- a Transform certainly does. The tolerance is relative,
because an absolute epsilon is wrong across the mm-to-metre range of real
parts, and a cut that shaves a numerically invisible sliver is a miss
too. The existing rollback in the MCP actions already preserves the
reason, so a missed hole now answers ok:false with the error and undoes
the feature.
The confusion underneath was not itself a bug: hole's x/y are in the
sketch plane's frame, whose origin is describe_scene's modeling_origin,
and describe_tools documented them only as "number, unit mm". Passing the
world centre put the hole 135 mm clear of the solid. All six x/y params
on hole / hole_styled / hole_standard now say which frame they are in,
since the wrong guess was silent.
Regression test drives the reported failure directly: a hole at x=135 on
a 20x20x20 box is rejected and leaves the body untouched, the same hole
at the origin still removes exactly pi*4^2*20, and a cut-mode extrude
whose profile sits at x=200 is rejected too. 152 cases / 2083 assertions
green on both forks.
snaporca-daf.
Like the BBL tower: the layer's sparse infill, wall, and brim go to the
first toolchange to a non-support/non-soluble filament, or are printed
with the incoming filament before any toolchange. The minimal-purge
clamp now also covers toolchanges that get no finish-layer saving.
Output is unchanged when no support/soluble filament is used.
All 17 single-letter sketch shortcuts were dead. The dispatch gate was
circular: the sketch key map was consulted only when
m_viewport->is_sketching() was already true, but is_sketching() is a
whole-session flag whose only riser is begin_sketch(), called from
select_tool() -- which is precisely what every sketch key closure calls.
So the first letter after entering sketch mode fell through to the
feature map, where the keys are Shift+letter, matched nothing, and did
nothing. The mouse worked only because the toolbar flyout row reaches
select_tool() directly, bypassing the gate.
Which key MAP applies is a question about the mode. Split the flag:
'sketching' (live session) still drives undo/redo, Delete and the
section-view branch, where a session genuinely has to exist; a new
'sketch_mode' (m_ui_mode == UiMode::Sketch alone) drives the map choice.
Verified headless end to end, keyboard only: Shift+S, then R draws a
143.4 x 133.7 rectangle on XY reporting 4 degrees of freedom, then F and
L arm Fillet and Line (artifacts/shots/0udb-01..03). Note the toolbar
strip does NOT change when a tool is armed by keyboard -- the family
buttons are flyouts and only show their own pressed state -- so the
pixel diff on that strip, which is how this was originally measured,
reads 0 for a tool that is live. The status line is the surface that
actually reflects the armed tool.
Also fixed, from snaporca-d9i's list: the plane-pick status line said
"press Sketch to draw on it", naming a button that exists only in
Feature mode. It is now mode-aware.
Adds a SNAPORCA_KEYTRACE=1 trace in the CHAR_HOOK printing key, ui mode,
is_sketching, in_text and the focused window's class. It is what
separated "the fix does not work" from "the surface being measured never
moves", and it costs a full GUI build to re-add, so it stays.
snaporca-0ud, partial snaporca-d9i.
Two P0s in the same gesture. Filleting a corner of a parametric rectangle
produced either nothing at all or a sharp corner with a stray arc floating
above it.
Trigger (snaporca-cq2): the only routes that ever reached confirm_op were
finishing the whole sketch and an unsignposted click on empty space.
set_tool() dropped a ready op, so typing a radius or dragging the arrow and
then touching any other tool threw the value away. Commit a ready op on tool
change (before m_mode is reassigned — op_ready() and confirm_op() both switch
on it), commit on Enter in the radius editor, and drop the pending op before
Esc's tool downgrade so Esc still cancels rather than applies.
Substitution (snaporca-pl5): libslvs writes its last Newton iterate into the
params whether or not it converged, and SketchSolver read them back
unconditionally, so every REJECTED solve deformed the sketch. The fillet
ladder tries a deliberately over-constrained rung first (a tangent on each
leg, against the legs' own H/V); it is correctly rejected, but its wreckage
then failed rungs 2 and 3, which solve cleanly on their own. The arc ended up
with no constraints at all, the rigid loop won, and the corner snapped shut.
Measured: from pristine geometry rung 1 gives result=INCONSISTENT with 3 bad
constraints, rung 2 gives dof=6 with the arc's radius intact.
Read the geometry back only on success. try_add_constraints then needs no
"restore" re-solve — the entities still hold the prior solved state.
Kernel suite 151 cases / 2072 assertions green on both forks; the GUI check
ran on the Snapmaker fork (9d72c4377a).
Ported from the Snapmaker fork. snaporca-pl5 snaporca-cq2
The card decides between a single picked edge and a whole face-group from a
viewport pick it never mentioned. With no edge picked the user saw only the
group combo and concluded per-edge rounding did not exist; with an edge picked
the combo still read "All" — the opposite of what Confirm would do.
Adds a Target row that names the actual target and greys the group combo out
while an edge is picked, wired at the same three points Shell already uses:
the selection-changed handler, the re-edit load, and open_tool.
Ported from the Snapmaker fork (33771a0e95). snaporca-40d
estimate_wipe_tower_polygon reserved the arrange footprint and clamped the
tower X position with the raw prime_tower_width, under-reserving space
whenever the rib wall squares the tower to a different width.
The rib tower is now always square (prime_tower_width is ignored, as the
GUI already implies), carries the rib origin offset like the BBL tower so
the rib tips sit inside the configured position, clamps the rib length to
the tower diagonal, and extends the ribs for short towers.
The CAD sources are meant to be byte-identical across the two forks, with exactly
two permitted divergences: DesignPanel.cpp's flyout plumbing (30 lines — mainline's
DropDown is Item-based where the other fork takes three parallel vectors) and
DesignCanvas.cpp's Bed3D::set_shape signature (16 lines). DesignPanel.cpp had drifted
to 105.
Nothing failed to announce this. The kernel suite does not compile the GUI, and all
four defects are interaction-level, so both forks stayed green while only one of them
had the fixes. The parity diff is what found it.
* combo_append_index and its five call sites. Orca's ComboBox keeps client data in
its own vector, so Append()'s clientData argument never reaches wxItemContainer
and m_clientDataItemsType stays wxClientData_None. GetClientData() opens with a
wxCHECK_MSG, which is an early return — so every read came back NULL and every
caller resolved it to index 0, silently, because 0 is a legal answer. Affects the
rib sketch picker, the sheet-body picker, both mate coordinate-system pickers and
the sweep path picker.
* Wrap(240) over the card labels. wxStaticText never wraps itself, so a one-sentence
hint sets its card's minimum width to the width of the whole sentence and every
control in that card is clipped at the sidebar's right edge.
* Shell and Draft face-label initialisation in open_tool. Both read the face from the
live pick at Confirm time, but only the pick handler wrote their labels, so picking
a face and then opening the card left the card describing one operation while
Confirm performed another.
* SurfaceOffset and ThickenSurface added to build_candidate's negative list. Both read
a sheet body from their own combo and both had it overwritten by the picked solid's
index. The list is a negative one, so a tool that picks its own body breaks by
omission — noted in a comment now.
Also drops a stray <cstdio> left behind by a debug probe.
Verified by building the GUI here for the first time since these landed: 461/461,
liblibslic3r_gui.a links. DesignPanel.cpp is back to exactly 30 divergent lines and
every other CAD file is byte-identical again.
snaporca-7xx snaporca-aqu snaporca-gu9 snaporca-c03 snaporca-5pl
The inline editor special-cased Escape and Skip()ped every other key, so Tab fell
through to wx's default navigation. Its popup frame holds exactly one control, so
focus came straight back to that control with its text re-selected.
Type 60, Tab, 40, Enter — expecting to fill two dimensions — and the 60 is gone: Tab
neither committed it nor advanced, so the 40 just replaced the re-selected text. A
re-selected field is pixel-identical to a freshly opened one, so nothing on screen says
a number was dropped. Tab-to-next-dimension is what Onshape, SolidWorks and Fusion do,
which is exactly why it is the key a user reaches for.
Tab now calls do_commit(), the same path Enter takes; the caller's on_commit is already
what walks to the next dimension. Verified by driving the GUI: 37 Tab 24 Enter now
produces a 37.0 x 24.0 rectangle, where before it produced 24 and a mouse-derived value.
snaporca-xah
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
A filleted solid arrived on the plate as a broken model: the slicer reported
"8 non-manifold edges" on an 80x50x12 box with r=3 on all edges, and advised
repairing it in another CAD application -- the exact round trip the Design tab
exists to remove. The same box without the fillet committed cleanly.
Measured rather than guessed. Each of the 8 bad edges is degenerate, both
endpoints the same vertex:
open tri=145 edge=1 face=5 v59(3.000000 3.000000 0.000000) v59(3.000000 3.000000 0.000000)
open tri=538 edge=1 face=6 v87(3.000000 3.000000 12.000000) v87(...)
... one per corner, 8 corners
OCCT triangulates a degenerate surface parameterization with a triangle at the
pole; a corner sphere patch has exactly one. Its two pole nodes are distinct in
the per-face triangulation and collapse to a single vertex when the faces are
welded, leaving a zero-area triangle whose v->v edge can never pair with a
neighbour. its_face_neighbors counts it as open, and the field the object panel
prints as "non-manifold edges" is in fact stats.open_edges.
So the geometry was never wrong -- the B-rep volume matches the Steiner formula
for a box dilated by a ball to 0.016%. Only the bookkeeping was.
Dropping those triangles after the weld removes 8 of 3492 and takes open_edges
to 0. Zero area, so nothing about the shape changes. tri_face is compacted in
the same pass, since it must stay index-aligned with the triangle list that the
face picking and per-body colouring both index into.
Guarded by a new [CadDocument] case that asserts open_edges == 0, no degenerate
triangle survives, and both per-triangle maps still match the triangle count.
The existing suite only ever checked B-rep volumes and areas, which is why a
mesh defect this visible went unnoticed: 150 cases / 2049 assertions green on
both forks.
snaporca-agw
OCCT_LIBS is an explicit single-pass static link order — dependents first, TKernel
deliberately last. The CAD block appended its two extra toolkits to the END of that list,
which puts them after everything they depend on:
list(APPEND OCCT_LIBS TKFillet TKOffset)
TKOffset references BRepAlgo_Loop, and nm against the built deps prefix shows TKBool is the
only toolkit that defines it (TKTopAlgo, TKBO, TKPrim, TKFillet and TKOffset all define it
zero times). TKBool sits first in the list, so a single-pass linker has passed it long before
it reaches the appended TKOffset and will not go back:
libTKOffset.a(BRepOffset_MakeLoops.cxx.o): undefined reference to
BRepAlgo_Loop::BRepAlgo_Loop()
Only one configuration ever objected — the Snapmaker fork Flatpak (aarch64). Ordinary Linux,
macOS and Windows links resolve it regardless, and the mainline fork Flatpaks pass, so six
green platform legs said nothing about whether this list was correct.
Prepended via set() rather than list(PREPEND), which needs CMake 3.15 while this project
supports 3.13.
Worth knowing for later: TKFillet and TKOffset are mutually dependent, 20 symbols needed in
each direction, so a stricter single-pass link could still trip on that pair. It does not on
any current platform, so no --start-group or duplicate entry is added here; if something ever
complains about ChFi or BRepFill symbols, that cycle is the reason.
snaporca-2kj
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
* Add wxInspector dep
* Initial intergration of wxInspector
* docs: add wxInspector plugins design spec
Design spec for two wxInspector plugins (DPIAware + CustomWidgets) that expose
OrcaSlicer's custom control properties in the inspector property grid.
Covers: DPIAware scale-factor properties, Button, CheckBox, TextInput,
SwitchButton, ProgressBar, Label, and LabeledStaticBox.
* docs: add wxInspector plugins implementation plan
6-task plan covering: source changes to existing widget headers,
DPIAwarePlugin, CustomWidgetsPlugin, registration helper,
MainFrame/CMake wiring, and build verification.
* feat: add getters/setters for wxInspector plugin access
Add minimal public accessors to DPIAware (set_scale_factor,
set_prev_scale_factor, set_em_unit, force_rescale), Button
(GetStyle, GetType, IsSelected), CheckBox (IsHalfChecked),
TextInput (GetCornerRadius), and LabeledStaticBox
(GetCornerRadius, GetBorderWidth, GetBorderColor, GetScale).
* feat: add wxInspector plugin registration helper
Add RegisterOrcaInspectorPlugins() inline function that creates
and registers the DPIAwarePlugin and CustomWidgetsPlugin as
static instances (matching wxInspector's built-in pattern).
* feat: add DPIAware wxInspector plugin
Exposes DPI scaling properties (scale_factor, prev_scale_factor,
em_unit, normal_font, force_rescale) on DPIFrame and DPIDialog
widgets. Uses dynamic_cast for detection and a template helper
to capture the correct static type for lambda accessors.
* feat: add OrcaCustomWidgets wxInspector plugin
Exposes Orca-specific properties on 7 widget types:
- Button: Style, Type, Selected
- CheckBox: Half Checked
- TextInput: Label, Text Value, Corner Radius
- SwitchButton: Value
- ProgressBar: Proportion, Show Number
- Label: Is Hyperlink, Font Point Size
- LabeledStaticBox: Corner Radius, Border Width, Border Color, Scale
Each widget type uses dynamic_cast for safe detection.
* feat: wire wxInspector plugins into MainFrame and build
Call RegisterOrcaInspectorPlugins() in MainFrame constructor after
SetupInspectorAccelerator(). Add all 5 plugin source files to
SLIC3R_GUI_SOURCES in CMakeLists.txt.
* fix: move plugin registration to GUI_App::on_init_inner
Register plugins once in app init rather than in MainFrame
constructor, which may be recreated during the application
lifetime.
* fix: include plugin headers in Registration.hpp for complete types
Static locals require complete type. Include DPIAwarePlugin.hpp and
CustomWidgetsPlugin.hpp instead of forward-declaring. Also remove
unused include from MainFrame.cpp (registration moved to GUI_App).
* fix: qualify DPIFrame/DPIDialog with Slic3r::GUI namespace
* Make DPIDialog inspectable. For other dialogs, we will add them if necessary later.
* docs: add spec for moving wxInspectable into DPIAware template
Move wxInspector::wxInspectable base class from DPIDialog and MainFrame
into the common DPIAware<P> template, making all DPIAware widgets
automatically visible in the inspector tree.
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: add implementation plan for moving wxInspectable into DPIAware
Co-Authored-By: Claude <noreply@anthropic.com>
* docs: update spec/plan — move SetupInspectorAccelerator into DPIAware too
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: move wxInspectable and SetupInspectorAccelerator into DPIAware
DPIAware<P> now inherits wxInspector::wxInspectable and calls
SetupInspectorAccelerator in its constructor, making all DPIAware
widgets automatically appear in the inspector tree with the
Ctrl+Shift+I shortcut. DPIDialog now uses 'using' to inherit the
constructor. Remove redundant wxInspectable inheritance and
SetupInspectorAccelerator calls from DPIDialog and MainFrame.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: use LB_HYPERLINK constant instead of magic number 0x0020
Co-Authored-By: Claude <noreply@anthropic.com>
* Clean up
* Fix Linux build
* Don't build wxInspector sample
* Use shallow clone
* Try fix flatpak build
* Attempt to fix build again
* Fix build failure caused by https://github.com/wxWidgets/wxWidgets/commit/436c16135ec7ddf580f44624bf74c592aae43b66
* wxWidgets build only download required submodules
* This should fix build on Windows on ARM
* Enable PIC
* Disable layout inspector by default for public release
* Use wxInspector 1.0.0 release
---------
Co-authored-by: Claude <noreply@anthropic.com>
The header forward-declares a long list of wx types and omitted wxBoxSizer, wxTextCtrl and
wxListCtrl. All three are used as pointer members only (m_expr_text, m_var_list,
m_parts_hdr, m_hdr_tree_row), so a forward declaration is all they need — but there was
none. Every ordinary build compiled anyway because the wx/panel.h + wx/scrolwin.h chain
happens to pull the real headers in transitively.
The Snapmaker fork Flatpak build (aarch64) has a wx that does not, and it failed outright:
DesignPanel.hpp:511: error: 'wxTextCtrl' does not name a type; did you mean 'wxTreeCtrl'?
DesignPanel.hpp:521: error: 'wxListCtrl' does not name a type; did you mean 'wxFileCtrl'?
DesignPanel.hpp:663: error: 'wxBoxSizer' does not name a type; did you mean 'wxSizer'?
plus a cascade of "m_var_list / m_expr_text / m_parts_hdr was not declared in this scope".
Not an environment quirk: the header was simply not self-contained, which is exactly what
breaks a reviewer building in an unfamiliar configuration. The mainline fork Flatpaks passed
on both arches, so only that one manifest exposed it.
Audited the rest of the header afterwards: every other wx pointer type is either
forward-declared or genuinely included — only wxScrolledWindow and wxWindow are undeclared,
and both come from the real wx/scrolwin.h include.
snaporca-4dn
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
store_bbs_3mf reaches Model::get_backup_path(), which builds
temporary_dir() + "/orcaslicer_model/" + timestamp. temporary_dir() returns a file-static
that ONLY OrcaSlicer.cpp's startup sets, so in a test binary it is the empty string and the
backup path becomes "/orcaslicer_model/..." — absolute, at the filesystem root. An
unprivileged process cannot create that, so the save returned false and the scenario died
on REQUIRE(store_bbs_3mf(sp)).
This was the SINGLE failure in this fork's Unit Tests — 1 of 566, on Linux x86_64, Linux
aarch64 and macOS arm64 — from CI run 30191490709:
Failed to create backup path "/orcaslicer_model/Sun_Jul_26/08_49_41#5398#1":
boost::filesystem::create_directories: Permission denied [system:13]
It hid because that job had never run to completion on this branch before: every earlier
run was cancelled by the concurrency group first. It also passed on Windows x64, where the
drive-root path is writable, and it passes in the local build container, which runs as root.
Verified against the same defect in the Snapmaker fork by running the built binary as
uid 1000: permission denied before, 4 assertions passing after.
snaporca-vg8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
snaporca-tkz, the last quarantined test. Root cause read out of the vendored
source rather than guessed: slvs/constrainteq.cpp, Type::ARC_LINE_TANGENT does
ExprVector ap = SK.GetEntity(arc->point[other ? 2 : 1])->PointGetExprs();
so it dereferences the ARC'S ENDPOINTS. A full circle entity carries only
point[0], its centre. point[1] and point[2] are zero handles, FindById throws
"Cannot find handle", and the process ABORTS rather than failing the solve —
taking every later test in the binary with it. That is also the wrong equation
for a circle regardless: it only makes the line perpendicular to the radius at
an endpoint that does not exist.
CT::Tangent no longer hands a full circle to that constraint. For a circle it
emits PT_LINE_DISTANCE(centre, line) = radius, which is precisely what tangency
to a circle means. Arcs keep the ARC_LINE_TANGENT path they are built for.
One limitation, stated rather than buried: the slvs C API takes a constant
distance and offers no way to reference the circle's radius parameter, so the
radius is captured when the constraint is emitted. That is exact whenever the
radius is fixed or is simply not driven by another constraint in the same
solve, and re-solving restores tangency if something else moves it. Tying them
would need an auxiliary point constrained onto both the circle and the line.
With this and eeca6794e7, both quarantined tests are gone and the exclusion in
kernel-test.sh goes with them. A green run now means the whole CAD suite
passed, not "everything except the two we gave up on":
149 cases / 2043 assertions, no filters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-through from eeca6794e7. The header claimed two pre-existing failures
are excluded and named both; the internal-thread case now runs like any other,
so only the solver SIGABRT is left. A comment that lists a test which is no
longer excluded sends the next reader looking for something that is not there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
snaporca-kzy was filed as "internal thread cuts too little material". It does
not. Measured on the test's own fixture, a 40x40x20 box:
plain Ø12 bore removes 2261 mm3
internal thread removes 2157 = 1571 (minor bore) + 586 (groove)
apply_thread bores at the MINOR radius (radius - depth = 5) and then carves the
groove out to radius + depth = 7. A tapped hole therefore keeps the crests
between turns and holds MORE material than a plain clearance hole at the
nominal radius — which is what every real tapped hole does. The test asserted
the opposite, so it was asking for something physically wrong and had been
quarantined for it since it was written.
One hypothesis discarded on the way: that the shortfall was a tessellation
artefact, since chords on a helical surface undercut a concave bore. Exact
BRepGProp::VolumeProperties agreed with the tessellated volume to within
2.5 mm3, so that was not it and is not offered as a hedge.
The reference is now the tap-drill bore the thread actually starts from (Ø10),
against which the groove's 586 mm3 is the meaningful quantity — that is what
"the thread cuts" means. Test re-tagged [CadDocument][thread], so CI covers the
thread path again instead of skipping it.
Also documented the (void)internal in make_thread_profile. It reads like a bug
and is not: the V is the same shape either way and the caller decides, fusing
it onto a shaft or cutting it out of a wall. Someone "fixing" it to point
inward for the internal case would make the groove sweep already-empty bore
space and cut nothing — the exact failure the old comment described.
Suite 148 cases / 2035 assertions, with this test now among them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by sweeping the index-space defect class deliberately rather than by
hitting it: that class produced 4 of the 8 defects found by hand yesterday, so
it was worth auditing every combo in the panel that maps a row selection onto
a document index.
Most of it came back clean — the loft sidecar vectors are consistent at all
four read sites, the sheet-body pickers go through the helper everywhere, mate
connectors carry client data. Six did not. A stored target_body indexes the
body list AS IT WAS just before that feature ran during replay, but Transform,
Mirror, Thicken, Rib, Project and DeleteFace all populated their combo from
the live m_doc.bodies. Boolean and Cut already replayed to the right slot.
The failure is concrete: model a body, Thicken it, then Cut something later in
the tree. A Cut replaces one body with two, so every index at or after it
shifts. Reopen the Thicken and the combo lists the post-cut bodies while
selecting the pre-cut index — showing, and on confirm re-targeting, a
different body than the feature actually used. A Boolean that consumes its
tool body shifts them the other way for the same result.
fill_body_choice() does the truncated replay populate_body_choices() already
did, for the single-combo tools. Six call sites, and 60 lines of duplicated
population loops go with them.
Visible change when testing: re-editing an early feature now lists FEWER
bodies, because it lists only those that existed then. That is correct — you
cannot target a body that did not exist yet — and it is what Boolean and Cut
have always done.
The new kernel test pins the invariant the GUI now leans on: a Cut turns one
body into two, and replaying to just before it yields the earlier, shorter
list. If body ordering after a split ever changes, that assumption fails
loudly here instead of silently in a dialog.
NOT click-tested — GUI wiring, compile-verified only. Filed as snaporca-oz7
and added to snaporca-cfi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
on_edit_feature had two types falling into default: with "This feature type
can't be edited yet". Boolean was already handled, so the follow-up note was
stale on that point; the real gap was Cut and Import.
Cut now re-edits like any other feature: plane, offset and target body are
restored and the generic replace_feature path commits the change. The body
list is rebuilt with populate_body_choices(m_edit_index) for the same reason
Boolean does it — a Cut splits one body into two, so the live body list no
longer matches the one this feature's target index was recorded against.
Replaying to just before the feature makes the stored index land on the right
entry.
Import deliberately gets no dialog. An imported solid has no parameters to
re-edit: its geometry is rigid data read from a file, not something rebuilt
from numbers, and moving it is what the Transform feature already does.
Building an "edit" for it would duplicate Transform behind a second name. So
it now says that instead — the previous message implied a dialog was coming
that should not.
Imported 2D Text/SVG art is a different thing and stays re-editable; it
arrives as a Sketch feature carrying imported_regions and is handled above.
The default: arm is kept as a guard so a feature type added later announces
itself rather than silently swallowing the click.
NOT click-tested — GUI wiring, compile-verified only. Added to snaporca-cfi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Shellcheck workflow has failed on every push and every scheduled run since
2026-07-24, on exactly one finding: SC2029 in scripts/kernel-test.sh, the file
the CAD branch added. So the CAD work is what turned that job red, and a PR
arriving with a red job is a bad way to open a conversation with a maintainer.
Client-side expansion of $REMOTE is the intended behaviour — it is derived from
$VOL locally and the remote has no such variable, exactly as the rsync
destination two lines down relies on. So this is a disable with a reason, not a
silencing: the note says why the warning does not apply.
Verified by running the workflow's own command over all 24 matched scripts:
exit 0, no findings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/design_tab_upstream_portability.md explains the subsystem to a
maintainer; nothing explained it to a user. This is that: what the tab is,
how to get a first solid out of it, every tool grouped the way the toolbar
groups them, and the keyboard shortcuts read out of the source rather than
remembered.
The limitations section is deliberate. Rib needing a sketch with an explicit
open line, Surface Loft and Surface Fill having no hands-on verification, mates
composing transforms instead of solving simultaneously, move-face and
replace-face being absent, and the two quarantined kernel tests are all things
a user would otherwise discover by hitting them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Maintainers will ask what the Design tab costs before they will look at the
diff, so measure it rather than assert it.
The headline correction: the OCCT delta is THREE toolkits, not two. The
comment in deps/OCCT/OCCT.cmake claimed "TKFillet + TKOffset (3.77 MiB,
Windows only)". Walking OCCT's own adm/MODULES and each toolkit's EXTERNLIB
shows ModelingAlgorithms holds twelve toolkits, that eight of them are built
either way because DataExchange (the STEP path upstream already ships) depends
on them, and that the true delta is TKFeat, TKFillet, TKOffset and TKXMesh —
of which TKXMesh is never produced. So three archives are built: 7.40, 5.38
and 4.42 MiB.
TKFeat is the interesting one. Nothing in the Design tab references it and it
is absent from the TKFillet/TKOffset dependency closure, so it is built for
nothing — OCCT's module flag is all-or-nothing per module. On static-link
platforms that is build time and zero shipped bytes.
Two numbers are deliberately absent, marked as absent, and not approximated:
the Windows DLL delta needs a Windows build (snaporca-gix), and a clean-build
time delta needs the deps prefix built twice on one machine. The old 3.77 MiB
figure is withdrawn rather than reused — it covered two of the three toolkits.
Also recorded: the vendored solver is 9,339 lines under GPLv3 with its LICENSE
preserved, which combines into this AGPLv3 fork without difficulty (AGPLv3
§13), and it is live code driving every sketch constraint — not a carried
corpse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DesignPanel.cpp was never listed in localization/i18n/list.txt, and xgettext
extracts only what that file names. All 934 of the Design tab's _L() calls
were therefore invisible to every translator in every language — not merely
untranslated, unextractable. Adding the one line is the actual fix; the rest
follows from it.
Regenerating the .pot brings the catalogue from 6007 to 6536 msgids. 549 of
the new ones are now translated into Italian: 5230 to 5779 translated
messages. Verified no existing work was destroyed — every msgid that survived
into the new .pot kept its translation, and the 36 that lost one are genuinely
gone from the sources. msgfmt --check-format passes, which matters here
because a large share of these carry %d / %s / %zu.
CAD terms follow Italian CAD convention rather than literal glosses: Fillet ->
Raccordo, Chamfer -> Smusso, Draft -> Sformo, Rib -> Nervatura, Mate ->
Accoppiamento, Shell -> Svuotamento, Pattern -> Serie, Sheet body -> Corpo
superficie. Strings identical in both languages are deliberately left
untranslated so gettext falls back to the msgid.
The long mixed-filament / Local-Z dithering tooltips are left untranslated on
purpose: slicer internals, outside a Design i18n task, and untranslated before
this commit too.
One string changed rather than translated. The Coord Sys hint read "Without an
edge the frame's rotation about its normal follows world X, not the body" —
that described the defect fixed in 1726e93760, so it was a lie as of that
commit. It now says X comes from the face's first edge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
datum_frame took a FaceAndDirection frame's Z from the face normal, which
follows the body, but its X from coordsys_x_hint, a world constant, whenever
no explicit edge reference was set. Spinning a body about its own face normal
therefore left the frame bit-identical: the connector could not encode that
rotation at all, so Fastened and Slider mates claimed to fix an orientation
the frame could not see.
X now comes from the face's own first usable edge, which rotates with the
body. The hint survives only as a last resort, for faces that offer no
in-plane direction — a full circular edge has coincident endpoints, and a
seam projects to nothing in-plane.
The new test spins a box 90 degrees about its top-face normal and asserts the
frame's X turned with it. Reverting just the X_tent derivation and rerunning
makes it fail with "1.0 is within 0.000001 of 0.0" — cos(angle) between the
before and after X is exactly 1, i.e. the frame did not move — and that is the
only failure in 2019 assertions, so the test discriminates this defect and
nothing else.
Note for anyone replaying an older document: a face-only connector's frame
can now differ from what that recipe produced before, so a mate built on one
may place its body differently. Nothing in the suite or the golden v3 fixture
changed, but the semantics did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This fork's Unit Tests job failed on every single commit, because CI runs the
whole ctest suite including the two cases tagged [known-broken] that
scripts/kernel-test.sh has always excluded locally. A job that is red
unconditionally is worse than no job: it trains everyone to ignore it, so the
next genuine regression arrives invisible.
No workflow change was needed. scripts/run_unit_tests.sh already passes
-LE NotWorking, and tests/CMakeLists.txt registers Catch2 tags as ctest labels
via catch_discover_tests(ADD_TAGS_AS_LABELS) — so the exclusion upstream
already ships works as soon as the cases carry the tag. Verified against the
built test tree: 337 tests unfiltered, 335 with -LE NotWorking, i.e. exactly
these two dropped and nothing else.
The second cause recorded in the issue, the test-reporter step failing with
"Resource not accessible by integration: 403" on a fork, is already fixed
upstream: the Publish Test Results step now carries continue-on-error: true.
The comment these cases carried claimed CI kept the bugs visible by reporting
them forever. That is now false and was never a good mechanism anyway, so
visibility moves to the tracker: snaporca-tkz for the solver SIGABRT, and
snaporca-kzy, filed now, for the thread groove volume. Neither is fixed;
neither is forgotten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rib guarded with `sk.type != CadFeatureType::Sketch`, while every other
sketch consumer — Extrude, SurfaceExtrude, SurfaceRevolve, the loft paths —
tests `!= Sketch && != Project`. A Project feature carries a plane and Line
entities, which is all a rib reads, so the guard blocked "project a body
edge, then rib along it" for no stated reason.
The picker in the Design tab offered Sketch features only, so it is widened
to match: a kernel that accepts Project refs and a GUI that never lists them
would have left the path unreachable anyway.
Worth recording for whoever hits this next: Rib also needs a sketch carrying
EXPLICIT entities. A parametric Rectangle sketch (add_sketch with
width/height) has an empty entities vector — build_sketch_wire synthesises
its profile on demand — so rib_entity 0 is out of range there and it fails
with "rib: bad entity". That is why Rib could not be driven headlessly at
all before this change; a Project feature is now the one programmatic way to
produce a ribbable line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects found while driving the tools that Phase B wired but nobody had
exercised yet.
apply_project had no all-edges branch: with no face picked and no explicit
edge list it threw "no edges or face selected". That is precisely the state
the Project card opens in, and its label reads "(all edges)" — so the card's
default could never be confirmed. It now projects every edge of the source
body. Edges perpendicular to the target plane collapse to a point when
projected, so segments whose endpoints coincide are dropped instead of being
emitted as zero-length lines that would poison the sketch downstream.
The second defect is why the first one was invisible. 29 of the 31 rollback
sites in McpControl ran `if (!ok) doc.undo();`, and undo() recomputes the
restored feature list — which succeeds and clears doc.error. Every failing
command therefore reported `error: ""`. Yesterday's fix covered 2 sites and I
treated the file as done; it was not. All 31 now capture the reason before
the rollback and restore it after. Failures that read as `""` now read as
"rib: bad entity" / "surface-revolve: revolve failed".
Verified on the running GUI through the control socket: the Project call that
previously returned ok:false now returns ok:true, and failures carry a reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six of the surface entries, both Thicken variants, Rib, Axis, Coord Sys,
Mate and Delete Face all reused a sibling's glyph, so a drawer opened as a
column of identical faces and the card that opened rarely matched the entry
clicked. Fixed both halves: entry icons are now unique within their drawer,
and each card header uses the icon of the entry that opens it.
Two new glyphs, design_thicken and design_rib, are the only ones added —
everywhere else an existing icon already carried the right meaning
(design_revolve, design_offset, design_line, design_point,
design_c_coincident, design_delete).
The Surface drawer BUTTON deliberately keeps design_surface; only its
entries may reuse the solid glyphs, because a menu row carries its own
text label while two adjacent toolbar buttons do not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
refresh_preview() exempted only Sketch and Plane from the ghost-preview path.
Axis, CoordSys, Helix and Project produce no solid either, so they fell through
to it, found nothing, and reported "invalid: preview produced no geometry" —
which also DISABLED Confirm, so all four tools were unusable rather than merely
noisy. Guaranteed on an empty document; with a body present the ghost path
finds something and masks it, which is why it survived until the tools were
tried on a fresh project.
All six non-solid tools are now exempt, each with its own ready message. The
list is not a guess: recompute() skips Sketch, Helix, Plane, Axis and CoordSys
outright and routes Project through apply_project(), which emits sketch entities
and no solid — so the panel and the kernel now agree on exactly what is not a
solid. Mate already had its own branch, since it needs Confirm gated on having
two distinct CoordSys features.
Introduced when Axis/CoordSys (batch 1) and Helix/Project (batch 3) were wired
without extending this exemption. Confirmed fixed on hardware: Axis ->
Plane Intersection with XY and XZ now resolves on an empty document, which also
exercises 60b04feea1.
Compiles clean; kernel untouched, suite unaffected at 143 cases / 1980
assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
axis_plane_a/b are filled by the GUI from populate_plane_choices(), whose rows
are XY / XZ / YZ followed by the datum planes, and the row is stored verbatim.
The kernel's base_plane() indexed datum_planes[ref] directly, so the two spaces
were off by three: picking XY resolved to datum plane 0, and picking the first
datum ran past the end and failed with "plane ref not found". The
PlaneIntersection axis type could not work from the GUI at all.
base_plane() now uses the encoding CadFeature::plane_base already uses — 0/1/2
are the base planes through the modeling origin, >=3 indexes datum_planes[ref-3]
— so there is one convention for plane references instead of two. That also
makes two base planes usable, which the previous code rejected as out of scope
even though XY x XZ is an ordinary way to define the X axis.
Removed the dead find_plane lambda directly above it. It was never called and
half-anticipated this exact offset ("if (ref >= 3) // base plane offset"),
which is presumably where the confusion started.
Tests: the existing parallel-planes case encoded the OLD convention, passing
axis_plane_a = 0 to mean "datum 0" — values the GUI cannot produce — so it is
re-based onto rows 3 and 4. Two new cases cover what the GUI actually emits:
base x base (XY x XZ -> X) and base x datum, the latter pinning the +3 offset.
Both verified to FAIL against the previous indexing, at test_caddocument.cpp
:2325 and :2346.
Found by auditing the remaining tools for the index-space defect class that had
already produced three bugs in the GUI; this is the first instance of it
crossing the GUI/kernel boundary.
Suite 143 cases / 1980 assertions (was 141/1972). GUI compiles clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Surface drawer used design_extrude, the same face as the Add-material
drawer, so the two buttons were indistinguishable in the feature bar.
design_surface.svg is a draped patch — deliberately unlike design_plane (a flat
parallelogram) and design_extrude (a box with an up-arrow) — with a faint
interior rule so it reads as a skin rather than a solid face. Same visual
language as the other 71: 24x24, no fill, #b6b6b6, stroke-width 0.85, round
caps and joins.
The five surface ENTRIES that also used design_extrude now use it too. That is
not cosmetic tidying: a flyout button's face follows the last-picked entry
(SetBitmap_(icon_names[i])), so changing only the drawer's default icon would
have been undone the moment the user picked anything. Surface Loft keeps
design_loft, which already suits it.
The six rows still share one glyph between them, so they are told apart by
label alone inside the flyout. Per-entry icons belong with snaporca-vrg
(Draft/Shell reusing design_dressup), not here.
Compiles clean; kernel untouched. Icon confirmed legible on hardware.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
checkpoint() snapshotted `features` and undo() restored `features`, but
`variables` is a separate member of CadDocument. Every caller of the documented
checkpoint -> mutate -> recompute -> undo-on-failure pattern therefore failed to
roll a variable back: the bad value stayed in the document and every later
recompute failed, which is exactly the corruption the pattern exists to prevent.
Feature `expr` bindings were unaffected only because expr lives inside
CadFeature and rode along in the features snapshot — which is why the feature
side appeared to work.
This was a kernel gap, not a GUI one: McpControl::action_set_variable has the
same sequence and was equally broken.
The undo/redo stacks now hold a {features, variables} Snapshot. Nothing here is
serialized, so no recipe version change and no golden-fixture regeneration.
Two tests, both verified to FAIL against a faithful reproduction of the bug
(undo() leaving `variables` untouched) at test_caddocument.cpp:4420 and :4441:
one covers restoring a variable's previous value, the other covers removing a
variable that did not exist before the checkpoint. Worth recording that the
first mutation attempt was NOT faithful — it dropped the restore but kept
std::move(variables) into the redo stack, which empties the map as a side effect
and made the second test pass for the wrong reason. A mutation has to reproduce
the original defect, not merely break the code.
Second defect, same area: undo() calls recompute(), which succeeds and clears
doc.error, so the reason an edit was rejected was destroyed before anything
could display it. Six sites — four in DesignPanel, two in McpControl — now carry
the message across the rollback. on_remove_variable additionally asserted
"referenced by a feature expression" as fact; it now offers that as the likely
cause and appends the real error, since that diagnosis is wrong for any other
failure.
Suite 141 cases / 1972 assertions (was 139/1960). GUI compiles clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sheet-only pickers filtered correctly and then threw the filtering away. Their
rows are the SHEET bodies, but GetSelection() was passed straight through as an
index into m_doc.bodies. With a solid at 0 and a sheet at 1 — the normal order,
since you extrude a solid before making a surface — the single row is row 0 but
body 1, so Surface Offset and Thicken Surface targeted the SOLID. The kernel then
refused with "target is not a sheet", which reads as a kernel bug rather than a
picker bug, and the row's own label ("Body 2") disagreed with what was targeted.
Four sites per tool were wrong, including the re-edit path, which compared a body
index against the sheet-only row count and so restored the wrong row.
populate_sheet_body_choices() now carries the real body index in client data, and
two helpers make the row/body distinction hard to get wrong again:
sheet_choice_body() reads it back, select_sheet_choice() finds the row holding a
given body. No caller touches GetSelection()/SetSelection() on these pickers.
This is the third instance of the same index-space confusion in this file, after
the 0-based body labels in the interference report and the Rib sketch picker. The
kernel suite cannot catch any of them: the kernel receives whatever index the GUI
computed, and its own tests pass correct ones.
Delete Face was structurally right — its picker uses the all-bodies populate, so
its indices genuinely match, and accumulation appends with a running list. Two
gaps closed: clicking "Add picked face" with nothing picked was a silent no-op,
indistinguishable from a broken button, and the same face could be added twice,
putting a duplicate id into delete_faces that the defeaturing has no reason to
cope with. Re-adding is now a no-op with a message, not an error.
Both confirmed working on hardware. Kernel untouched: 139 cases / 1960 assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port of snaporca ca25352d74. Hand-applied rather than cherry-picked: unlike the
Phase B commits, which only touched DesignPanel.{cpp,hpp} and transfer verbatim,
this one reaches into GLCanvas3D and DesignCanvas, where the forks genuinely
differ — mainline passes m_show_world_axes to _render_bed where Snapmaker passes
a local show_axes, and the surrounding code sits ~90 lines further down. git am
refused, correctly; the six edits were applied against mainline's own context and
the DesignPanel half came across as a patch.
Bed toggle: a "Bed" checkbox in the document/view row, on by default, in that row
rather than in a card because a view option must stay reachable with no tool open.
It drives GLCanvas3D::m_show_bed (default true, so Prepare and Preview are
untouched) and gates _render_platelist as well as _render_bed — hiding the bed
while leaving its grid and outline floating would read as a rendering fault.
Bound to wxEVT_TOGGLEBUTTON, not wxEVT_CHECKBOX: Orca's CheckBox derives from
wxBitmapToggleButton, so a wxEVT_CHECKBOX handler never fires.
Also gives the Placement drawer its own "placement" toolbar slot. put("place")
already holds the Place-on-Face button, and put() formats slot item 0 as the
control and later items as its chevron, so sharing the slot bottom-aligned the
drawer's button like a chevron.
196/196 targets, 0 compile errors, orca-slicer links (165 MB). The build script
still exits non-zero at the AppImage bundling step on libpython3.12.so.1.0 —
that is snaporca-96t, packaging only, and does not affect the binary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar opened with eight tool cards stacked in it — Transform, Mirror,
Thicken, Rib, Project, Delete Face, Helix and Mate. A card added to the cards
sizer is visible until something hides it, and close_tool()'s hide-all only
runs on a tool SWITCH, so anything missing from the construction-time hide
block is on screen from the moment the tab opens. Those eight were wired into
close_tool() but never added here. All 37 cards are now hidden at startup.
Worth stating because it invalidates a check I ran while diagnosing this: every
card IS hidden somewhere in the file, so grepping for "hidden anywhere" says
nothing. The block that matters is the one in the constructor.
Second, the drawers mixed unrelated operations, and two group tooltips no
longer described their contents — Dress-up listed eight tools spanning three
different kinds of operation, and Add material still claimed to hold only
extrude/revolve/sweep/loft after Thicken and Rib were added to it.
One concept per drawer now:
Add material extrude, revolve, sweep, loft, thicken, rib
-> grows new solid material, whether from a profile, a face or
a line
Surface unchanged; already coherent
Datum / Curve plane, axis, coord sys, helix, PROJECT
-> reference geometry and derived curves. Project consumes a
body but PRODUCES sketch entities, so it is curve creation,
not a finishing operation
Placement TRANSFORM, MIRROR, MATE (new)
-> moves a body without changing its shape; a mate places one
body relative to another
Dress-up fillet/chamfer, draft, shell, delete face
-> finishing on the faces and edges of an existing solid
Hole / thread unchanged
The new drawer costs no toolbar width: the layout order already contained an
empty put("place") slot between "material" and "plane" with nothing registered
to it. Shift+Y and Shift+Z follow Transform and Mirror; every tool still
appears exactly once.
Compiles clean; kernel untouched, so the suite is unaffected at 139 cases /
1960 assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owned ("Mine") cloud plugins now offer the same local-only Delete as local
plugins: it removes the installed package and leaves the plugin in the cloud,
still reinstallable. Deleting a plugin from the cloud belongs on the plugin hub
and is no longer reachable from OrcaSlicer, so the whole cloud-delete chain is
removed down to the REST binding.
The deleted row is restored locally instead of via a blocking cloud refetch, so
it survives being offline, and it comes back without the deleted package's error
state.
M6 landed the kernel side as two plain public maps — CadDocument::variables and
CadFeature::expr — reachable only through MCP's set_variable / set_feature_expr.
Nothing in the GUI could create a variable, so the parametric layer was
unreachable from the Design tab.
Variables get a wxListCtrl (name, expression) with add/edit/remove below the
feature tree, since they are document-scope and must not live in a card that
only exists while a tool is open. Expression bindings get one generic row in the
feature-edit path — a field-name combo plus an expression box — rather than an
extra control on each of 32 cards.
Every mutation copies McpControl's sequence exactly, and the rollback is the
part that matters: checkpoint, mutate, recompute, undo() on failure. Without it
one typo leaves the recipe permanently unrecomputable, since load() replays the
whole list. Removing a variable a feature still references fails that recompute,
so it reports the reference rather than a bare evaluation error.
The field-name combo is deliberately editable: only 11 feature types get a
curated field list, and free text is what makes the other 21 reachable. That is
safe because assign_field() throws "unknown parameter: <name>" for anything it
does not know, inside recompute()'s try block — so a wrong name gives a clear
message and a rollback, never a silently dead binding.
Two fixes on top of the generated wiring:
- make_combo() passes wxCB_READONLY, under which Orca's ComboBox HIDES its text
ctrl (ComboBox.cpp:51). There is no SetEditable() to undo that, so the field
combo is constructed directly with style 0; that shows the ctrl with
wxTE_PROCESS_ENTER and makes GetValue() return typed text.
- the field-list helper had been made a file-static function taking
DesignPanel::Tool, which required moving Tool out of private and into the
public API. It is now a private static member instead: 32 values of internal
card state should not be published to satisfy a signature.
Compiles clean (0 errors); kernel suite unchanged at 139 cases / 1960
assertions. Phase B is complete on this fork — all 16 previously GUI-less tools
plus the variables panel. Not yet exercised on a display.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mate completes the set: every CadDocument feature type now has a card. It is the
one that needed CoordSys wired first, since a mate connector IS a CoordSys
feature and cs_a/cs_b are feature indices into the recipe.
The card states what each kind constrains rather than just naming it, because
the five kinds are not distinguishable from their labels — the useful fact is
which DOF each PRESERVES: Fastened fixes all six, Planar leaves in-plane
sliding, Revolute leaves spin, Slider leaves axial travel, Cylindrical leaves
both. The offset/angle spins retitle per kind, since offset is a plane distance
for Planar and a position along the axis for the three joint kinds.
Confirm is blocked, with the reason in the status line, when fewer than two
CoordSys features exist or when A and B are the same one: a mate with cs_a ==
cs_b is meaningless and a dangling index recomputes to nothing useful.
check_interference() gets a button in the feature-tree header behind a rule, not
a tool card — it adds no feature, so it must not checkpoint(), recompute(), or
touch the undo stack, and a card would imply it does. Results go to the status
line as count + worst volume, with the per-pair list in a message box, named as
the parts tree names them.
Fixes on top of the generated wiring:
- the button's sizer adds sat after the closing brace of the block declaring
trow, so trow was out of scope ("'trow' was not declared in this scope");
- the CoordSys client data was typed const void*, which Append rejects;
- the report labelled bodies 0-based while all 28 other body labels in this
panel (and the parts tree) are 1-based, so it would have called the tree's
"Body 2" an interference on "Body 1" — and it was the only unlocalised label.
Compiles clean (0 errors); kernel suite unchanged at 139 cases / 1960
assertions. Still to come: the M6 variables panel. The GUI has not yet been
exercised on a display.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every CAD feature added across M1-M8 got a kernel API and an MCP method, and
almost none got a card. The MCP path was the only way to reach them, so from
the Design tab these features did not exist: add_axis and add_coordsys have
been callable since M1 with no UI at all.
Wired here, in three batches:
datum Axis (5 construction types), CoordSys
surface Surface Extrude / Revolve / Loft / Fill / Offset / Thicken Surface
body Transform, Mirror, Thicken, Rib, Project, Delete Face, Helix
Grouped into existing dropdowns rather than widening the 16-slot toolbar:
surfaces get one new "Surface" dropdown, body ops join Dress-up, Thicken/Rib
join Add-material, Helix joins Datum (renamed "Datum / Curve"). Shift+Y and
Shift+Z went to Transform and Mirror; the remaining five are shortcut-less
rather than getting invented chords.
Three places where the UI has to encode a kernel distinction, not just expose
a field:
- Surface Offset and Thicken Surface consume a SHEET body and fail with "target
is not a sheet" on a solid, so their pickers filter on
CadDocument::is_sheet_shape() and say so when no sheet exists. Thicken (solid
face -> plate) is a different tool and is kept visibly separate.
- delete_faces is a vector, so Delete Face accumulates picks via "Add picked
face" and shows the running list. Supporting one face would have been a
silent downgrade of the kernel field.
- CoordSys labels its edge pick with the consequence of omitting it: without an
edge, datum_frame() takes x from coordsys_x_hint (world constant) and the
frame cannot express rotation about its own normal — which is snaporca-en4,
and is why a Fastened mate built on a face-only connector cannot fix spin.
The Rib sketch picker needed the 3-arg Append(text, wxNullBitmap, clientdata):
ComboBox's own Append(text, bitmap) hides wxItemContainer's (text, void*), so
the 2-arg call resolves to the bitmap overload and fails with "conversion from
void* to const wxBitmap is ambiguous". The Sweep picker already documents this;
Rib now matches it.
Compiles clean (0 errors) against snaporca-deps; kernel suite unchanged at
139 cases / 1960 assertions. Mate, the interference report and the M6 variables
panel are still to come; the GUI itself has not been exercised on a display yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First successful link of orca-slicer in this fork's history (158 MB, 738/738
objects, plus OrcaSlicer_profile_validator). 1633005bba got the test binary
green; the GUI had still never linked.
The blocker was in the deps image, not the code. That commit's message says
OCCT V7_6_0, Boost 1.84.0 and OpenCV 4.6.0 "are pinned identically in both
forks and were reused as-is". That is true of the VERSIONS and false of the
FLAGS: this fork's deps/OpenCV/OpenCV.cmake passes -DWITH_JPEG=OFF,
-DWITH_TIFF=OFF and -DBUILD_TIFF=OFF, and snaporca's does not. So the reused
build shipped lib/opencv4/3rdparty/liblibjpeg-turbo.a, which collides with the
deps' own lib/libjpeg.a — wx pulls that in via wxUSE_LIBJPEG=sys — on
jpeg_stdio_dest. Under -flto that duplicate is fatal, not a warning. snaporca
never sees it because its GUI does not link libopencv_world.a at all.
Fixed by rebuilding OpenCV 4.6.0 in orcacad-deps with this fork's own flags
(read out of the recipe rather than retyped), from the source already cached in
the image, into a clean build dir so no stale cache entry survived, and deleting
the orphaned bundled jpeg archive. The rebuilt libopencv_world.a has no jpeg or
tiff symbol references and its CMake config no longer names either library.
--allow-multiple-definition would have hidden this while leaving OpenCV carrying
codecs mainline deliberately turns off.
Lesson for the next dep: comparing deps recipes by version is not enough, diff
the CMAKE_ARGS.
Two more mounts, same root cause as the CMakeLists.txt mount this script
already documents — the baked tree is snaporca's:
- build_linux.sh, which builds `--target Snapmaker_Orca`; here the target is
OrcaSlicer and its output name is orca-slicer, so configure passed and ninja
then died on "unknown target". The binary check was looking for the wrong
name too.
- scripts/, because the packaging step needs scripts/appimage_lib_policy.sh;
without it a fully successful link still exited non-zero with "missing
AppImage helper" and the binary check never ran.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both scripts defaulted IMAGE to snaporca-deps. That image is Snapmaker-based
and lacks Eigen 5.0.1, CGAL 5.6.3, wx 3.3.2 and Python 3.12 Development.Embed,
so running either script here without an explicit IMAGE= dies at CMake
configure — which is a large part of why this fork reached M8 having never once
compiled (1633005bba). The volume defaults were fixed in that commit; the image
default was missed in both files.
docker-iter-build.sh also never mounted deps_src, so it could not have
configured even with the right image: root CMakeLists.txt:947 FATAL_ERRORs
when deps_src/pybind11/include/pybind11/pybind11.h is absent, and
src/CMakeLists.txt pulls semver/hints/imgui/imguizmo/hidapi from the same tree.
kernel-test.sh got that mount in 1633005bba; this is the same fix for the GUI
build path, needed before the Phase B GUI work can be ported here.
CadDocument.hpp: mate_kind comment, mirrored verbatim from snaporca dbaa104f62
so the header stays byte-identical across forks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Description
Changing a slicing plugin's configuration had no effect on the sliced
result until you forced a re-slice some other way; it now applies
immediately. Print, printer and filament presets also keep their plugin
configuration separately, so configuring a plugin on one no longer wipes
out what you set on another.
A plugin's custom configuration page gets the same round of improvements
in both the Plugins dialog and the per-preset dialog: it follows the
app's light/dark theme, keeps its state while you edit instead of
resetting under the cursor, and can tell whether it is being edited
globally or for a preset, so "Restore defaults" can be labeled for what
it will actually do. The two bundled examples show this off — Twistify
now ships a custom configuration UI, and Inspector is themed, groups
# Screenshots/Recordings/Graphs
https://github.com/user-attachments/assets/02ca062a-5143-49a3-abe0-a2a040b3a928
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
206/206 targets built, 139 [CadDocument] cases / 1960 assertions passing —
identical to snaporca's suite. Until now this fork had never compiled at all:
CMake died at configure, so the M1-M8 "suite green" figures were snaporca's
alone and the ports rested on patch-apply plus byte-identical sources.
Three fixes here; the deps work is in the orcacad-deps image (see below).
1. kernel-test.sh mounts deps_src. pybind11 is vendored in-tree and CMakeLists
requires its headers; without the mount the container fell back to the
image's baked tree, which predates it.
2. tests/libslic3r/test_3mf.cpp: repair the upstream-merge conflict resolution.
Resolving it as a union dropped the three closing braces of our SCENARIO, so
upstream's SCENARIO opened inside ours ("a function-definition is not allowed
here", plus 12 cascading catch2 registry errors). Restored from the pre-merge
file; whole-file brace balance is now 0 and the case count reconciles as
5 (ours) + 8 (upstream) - 3 (shared) = 10, with both CAD recipe tests intact.
3. tests/libslic3r/test_caddocument.cpp: REQUIRE_CONTAINS / CHECK_CONTAINS.
Catch2 v2 (snaporca) spells substring-match Matchers::Contains; v3 (here)
spells it ContainsSubstring and gives Contains an incompatible meaning,
range-contains-ELEMENT, which fails to COMPILE against std::string. Four
sites had been hand-adapted long ago, but M2-M8 kept porting in un-adapted
Contains calls — 16 of them — and nothing objected because nothing compiled.
Both forks now use the same find()-based macros, so the assertion lines are
byte-identical again and future format-patch ports carry across unchanged.
Five orphaned `using Catch::Matchers::Contains;` lines removed with them.
The deps gap that blocked configure needed five additions on top of
snaporca-deps, built into image orcacad-deps: Eigen 5.0.1, Python 3.12.13
(exact, with Development.Embed), wxWidgets 3.3.2 (was 3.1.5), CGAL 5.6.3
(was 5.4 — mainline's own MeshBoolean.cpp calls CGAL::parameters::default_values,
added in 5.5), plus the pybind11 mount above. OCCT V7_6_0, Boost 1.84.0 and
OpenCV 4.6.0 are pinned identically in both forks and were reused as-is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BUILD_VOL defaulted to snaporca_buildcache — snaporca's volume — so running
this fork's kernel test wrote into the other fork's build cache. Defaults to
orcacad_kerneltest now.
docker-iter-build.sh had the identical defect and was fixed to
orcacad_buildcache; this script was missed at the time.
Observed rather than theorised: an orca_cad run under a different deps image
overwrote snaporca_buildcache's CMakeCache.txt, after which snaporca's own
kernel test failed to configure ("Cannot find NLopt library 'nlopt_cxx' in
.../lib/cmake/nlopt/lib") because it inherited the foreign cached paths. No
foreign object files were written — the run died at configure — but the cache
was poisoned, and the volume had to be wiped and rebuilt clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Catches the fork up from 449a4cf9fc (2026-06-28) to d6cb667b89 (2026-07-24).
Upstream touched 2326 files; 17 of them overlap the 164 this branch touches.
16 of the 17 auto-merged, including all three CMakeLists.txt, the build_all.yml
CI workflow, and every GUI file. The CAD core never conflicts: CadDocument,
SketchEngine, SketchSolver, McpControl and test_caddocument are files this fork
adds, so upstream does not touch them.
The one conflict, tests/libslic3r/test_3mf.cpp, was purely additive in all three
hunks and is resolved as a union: our test pinning that store_bbs_3mf embeds the
CAD recipe as Metadata/SnapOrca_cad.bin, upstream's multi-nozzle plate-metadata
round-trip tests, and both sets of includes. All three were verified present
after resolution rather than assumed.
NOT BUILD-VERIFIED, for a reason that predates this merge and is not caused by
it: this fork cannot be configured on nativedev at all. Its CMakeLists has
required Eigen3 5.0.1 since before the merge (line 592 pre-merge), while the
only deps image on the machine is snaporca-deps, built for snaporca's
find_package(Eigen3 3.3). CMake fails at configure, so nothing compiles.
That means this fork's Catch2 suite has never run. Every "suite green" figure
recorded for M1-M8 was snaporca's suite; the ports were verified by patch-apply
plus the CAD sources being byte-identical to snaporca's. Building an orca_cad
deps image with Eigen 5.0.1 is what would finally close that gap.
Pre-merge state is preserved at branch cad-mainline-pre-upstream-2026-07-25
(30d54f0074).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check_interference(min_volume) reports every pair of solid bodies whose
intersection encloses more than min_volume, as {body_a, body_b, volume}. It
reports only — no geometry is mutated, so calling it cannot disturb mates or
placements. Read-only at the MCP surface too: no checkpoint, no recompute.
Sheet bodies are skipped up front: an intersection involving one encloses no
volume, so the boolean would be wasted work. Bodies that merely touch share a
face and enclose nothing, so face-to-face contact is not an interference.
A boolean that fails on one pair must not lose the report for every other pair,
so each pair is guarded — and OCCT raises Standard_Failure, which is not a
std::exception and would otherwise escape.
No new serialized fields, no recipe bump: this reads `bodies`, which is
recompute output and was never serialized.
No separate MCP listing for instances and mates: describe_scene already emits
the feature tree, and Mate has rendered there correctly since M8a fixed
feature_type_name.
Tests assert the exact overlap volume (20*20*4 = 1600 mm^3), both negative cases
(clearly apart, and exact face contact), that sheets are skipped, that the
min_volume gate silences a real overlap, and that a clash created by a Fastened
mate is detected — which ties the M8b placement work to this report.
Suite 139 cases / 1960 assertions green. McpControl.cpp is reviewed but not
compiled by kernel-test.sh, which builds only libslic3r_tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each kind constrains the DOFs it owns and PRESERVES the rest from the body's
current pose, following the pattern Planar established in M8a. Resolved instead
as "Fastened with a parameter", all three would have been geometrically
identical to Fastened — relabelling rather than behaviour.
Revolute fixes position on the axis line; rotation about it survives
Slider fixes orientation and perpendicular position; axial position survives
Cylindrical fixes the axis line only; rotation and axial position both survive
No new serialized fields, no recipe bump, no fixture regeneration: mate_kind is
already an int and mate_offset / mate_angle already exist.
The minimum-rotation z-alignment (including the antiparallel 180 deg case fixed
in M8a) is now a shared make_z_align lambda rather than a second copy.
Fixes a rotation-about-pivot bug found by the no-op tests: R_full was built as a
rotation about the origin with a translation to oB appended, instead of a proper
rotation about oB (translation = oB - R*oB). It moved bodies that were already
correctly placed, and accounted for three of the seven initially failing cases.
Testing notes, both of which cost real debugging time here:
- Mates are defined on connector FRAMES, but the convenient thing to measure is
CentreOfMass(), and the two coincide only when the body is symmetric about its
connector. Five expectations in this milestone asserted the centroid while
meaning the connector. These tests assert on the mated face's centroid.
- A CoordSys built from a face ALONE takes its z from the face normal (which
follows the body) but its x from coordsys_x_hint, a world constant. Such a
frame cannot see rotation about its own normal, so no mate can correct or
preserve a spin it does not encode. The Slider and Cylindrical rotation tests
pin coordsys_edge to an edge of their own body; without that both passed
vacuously, one of them for a wrong implementation.
The Cylindrical rotation test was verified to fail when its mate kind is mutated
to Slider, and the Slider test failed at axis_aligned == 2 before the connectors
were edge-pinned. Neither is green by accident.
Known wart: mate_angle is silently ignored for Slider, whose rotation is fully
constrained. Defensible but undiagnosed at the API surface.
Suite 134 cases / 1927 assertions green. McpControl.cpp is reviewed but not
compiled by kernel-test.sh, which builds only libslic3r_tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An assembly is a multi-body document. An instance is already expressible as
Transform with xf_copy=true, and a mate connector is already a CoordSys
feature, so this adds exactly one feature type: Mate.
No constraint solver. A mate rigidly transforms the body carrying connector B
so that B's frame lands on connector A's, applied in feature order like every
other feature. Chains resolve by composition; closed kinematic loops do not
converge (last mate wins) and are out of scope.
The vendored SolveSpace in src/libslic3r/slvs/ was evaluated for 3D extension
and rejected: it is built and linked but has zero callers, and SketchEngine's
solver is hand-rolled. Extending it would mean adopting a dependency to write
more code than the alternative.
- CadFeatureType::Mate appended; six fields (mate_kind, mate_cs_a, mate_cs_b,
mate_offset, mate_angle, mate_flip) appended at the END of both cereal lists
- SNAPORCA_CAD_RECIPE_VERSION 2 -> 3; v2 blobs are rejected, as by design there
is no migration path. Golden fixture renamed to cad_recipe_v3.bin and
regenerated once, extended with two CoordSys + one Mate so the new fields are
tripwired by the field-order assertions
- datum_frame() extracted from resolve_datum_coordsys() so a mate can resolve
its connectors against the in-progress bodies vector during replay
- apply_mate dispatched early-return, so Mate is deliberately absent from
starts_new (unreachable for that dispatch style)
- Planar: the degenerate branch splits on the sign of zB.z_target — antiparallel
needs a 180 deg rotation about a perpendicular axis, which an earlier revision
silently skipped, leaving the body's normal inverted
- MCP: mate command, named bare to match the other 38 methods
Drive-by: feature_type_name() was missing Mirror, ThickenSurface, SurfaceOffset,
SurfaceLoft and SurfaceFill, which reported as "Unknown" to MCP clients.
Suite 122 cases / 1741 assertions green. Note that kernel-test.sh builds only
libslic3r_tests, so McpControl.cpp is reviewed but not compiled here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two remaining ways to create a sheet body. SurfaceLoft skins 2+ profile
sketches without end caps via a new SketchEngine::make_loft_surface — a
sibling of make_loft with the ThruSections solid flag false, so no existing
call site changes. SurfaceFill patches a single closed boundary wire into a
smooth face with BRepOffsetAPI_MakeFilling, adding each boundary edge as a
C0 constraint.
Purely additive: two enum values appended to CadFeatureType, reusing the
existing loft_profile_refs/loft_ruled and sketch_ref fields. No new cereal
fields, recipe stays v2, golden fixture unchanged (30773). MCP
surface_loft/surface_fill added as pure additions. Suite 107 cases / 1553
assertions green, including a test contrasting the open skin against the
solid loft of the same profiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Two features bridging sheet bodies back to solids and to other sheets:
ThickenSurface feeds a whole sheet shell to MakeThickSolidBySimple (the same
OCCT recipe the face-level Thicken already uses) and appends the result as a
solid; SurfaceOffset offsets a sheet's shell along its normals via
MakeOffsetShape::PerformBySimple, keeping it open. Both refuse a non-sheet
target with a clear error.
Purely additive: two enum values appended to CadFeatureType, reusing the
existing target_body / thicken_thickness / thicken_flip / plane_offset
fields. No new cereal fields, recipe stays v2, golden fixture unchanged
(30773). MCP thicken_surface/surface_offset added as pure additions.
Suite 103 cases / 1520 assertions green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Two body-producing features that emit an open shell instead of a capped
solid: SurfaceExtrude (prism of a sketch wire, no end caps) and
SurfaceRevolve (revolve of a wire about an in-plane axis, no caps). Each
appends a new sheet body whose TopoDS_Shape has no TopAbs_SOLID.
Purely additive: two enum values appended at the end of CadFeatureType,
reusing existing serialized fields (sketch_ref/distance,
revolve_angle/revolve_axis). No new cereal fields, recipe stays v2, golden
fixture unchanged. is_sheet_shape() derives sheet-ness from the OCCT shape
type (bodies are not serialized). MCP surface_extrude/surface_revolve added
as pure additions. Suite 99 cases / 1474 assertions green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Add named document variables (CadDocument::variables) and per-feature
expression bindings (CadFeature::expr, field-name -> expression). On
recompute(), variables are evaluated topologically (cycle detection), then
each feature's expr entries are evaluated and written into its numeric fields
before geometry runs. Self-contained shunting-yard evaluator (+ - * /, parens,
unary minus, sqrt/abs/sin/cos/tan(deg)/min/max, pi). assign_field allow-lists
the 33 dimension fields + pattern_count; unknown names error loudly.
Additive: recipe stays v2 (fields appended to both cereal lists, golden
fixture regenerated). MCP set_variable / set_feature_expr are pure additions.
Suite 95 cases / 1440 assertions green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Extend CadFeatureType::Pattern (no new enum) with a curve mode: when
pattern_curve_sketch >= 0 it takes precedence over linear/circular. The guide
entity is sampled at equal-parameter points via a file-local sample_entity_2d()
(Line lerp, Arc angle-lerp, cubic-BSpline Bernstein, p0->p1 fallback), and each
seed copy is translated by (P_i - P_0) and fused. Two serialized fields
(pattern_curve_sketch/pattern_curve_entity) appended to both symmetric cereal
lists (version stays 2, golden fixture regenerated 30269->30517). MCP:
pattern_on_curve. 3 new [CadDocument][pattern] tests; suite 89/1395.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
New CadFeatureType::Rib (appended). A straight open Line entity in a sketch
is offset ±thickness/2 along its in-plane perpendicular into a thin rectangle,
extruded rib_depth along the sketch-plane normal, and fused to the target body.
Line-only for now (ponytail; polyline/arc ribs are a later extension) — a
non-line entity fails cleanly at recompute. Four serialized fields
(rib_sketch_ref/rib_entity/rib_thickness/rib_depth) appended to both symmetric
cereal lists (version stays 2, golden fixture regenerated 29525->30269). MCP:
rib. 3 new [CadDocument][rib] tests; suite 86/1376.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Extend CadFeatureType::Hole (no new enum value) with a style flag
(simple/counterbore/countersink) and the matching geometry: a coaxial
shallow cylinder cut for counterbores, a cone-frustum cut for countersinks.
A file-local hole_std_lookup() resolves screw designations (ISO 273/4762/
10642 metric M3–M10 + common ANSI unified) into clearance/cbore/csink dims;
add_hole_standard() fills the feature from it, add_hole_styled() takes them
explicitly. Six serialized fields appended to both symmetric cereal lists
(recipe version stays 2, golden fixture regenerated 28161->29525). MCP:
hole_styled, hole_standard. 4 new [CadDocument][hole] tests; suite 83/1351.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
New CadFeatureType::DeleteFace: removes a set of global face ids from target_body
and heals the gap via BRepAlgoAPI_Defeaturing (TKBO, already linked), mirroring the
Shell/Draft body-modifying pattern. delete_faces appended to both symmetric cereal
lists (recipe version stays 2, golden fixture regenerated 27913->28161). MCP
delete_face method (pure additions). 3 new [CadDocument][deleteface] tests: remove a
fillet face restores the sharp-box volume, bad index fails safely, round-trip.
Full kernel suite green (79 cases, 1309 asserts).
Move-face / replace-face deferred to snaporca-3c4 / snaporca-tc6 (no clean shipping
OCCT direct-modeling primitive; need research, and replace-face depends on M7 surfaces).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Adds SketchEngine::make_bridge (4-pole cubic Bezier, G1-tangent to Line/Arc
endpoints, straight-line fallback for other types) emitted as the existing
BSpline SketchEntity — no new geometry type, no serialized-field change, golden
recipe fixture untouched. CadDocument::add_bridge appends it (non-parametric,
index-validated, throws on bad refs). MCP `bridge` method mirrors action_project.
4 new [CadDocument][bridge] tests; full kernel suite green (76 cases, 1280 asserts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The construction flag was already honored (excluded from the extrude wire in
SketchEngine.cpp, participates in the solver, and serialized) but the existing
"construction line excluded" test was a false tripwire: its construction line
ran corner-to-corner inside the square, so the bbox was unchanged whether or not
the line was excluded.
- Strengthen that test: the construction line now runs (-30,0)->(30,0) outside
the profile, so an exclusion regression breaks the closed wire / bbox.
- Add a serialize/deserialize round-trip test asserting construction survives.
- Lock the flag on-disk: add Sketch_Ctor to the golden fixture with a real edge
+ a construction edge, and assert both flags survive the binary recipe.
Test-only; no kernel change. Recipe version stays 2 (construction was already a
serialized field). Suite: 72 cases / 1246 assertions green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Onshape-style "Use / Convert entities": pick edges (or a whole face) of an
existing solid and get sketch geometry projected onto a target plane, then
extrude/revolve/edit it like any sketch. Parametric: apply_project re-derives
the feature's entities from the source body on every recompute, so editing the
source updates the projection.
Line edges -> Line entities (exact); circles/arcs whose plane is parallel to
the sketch plane -> Circle/Arc (exact); everything else (incl. non-parallel
circles that project to ellipses) -> sampled Line chain.
Append-only: new enum value Project + project_source_body/project_edges/
project_face fields at the end of save/load; recipe version stays 2. Recompute
loop made non-const solely so apply_project can write back f.entities.
Suite 67->71 cases, 1178->1229 assertions, RC=0. Fixture 25493->26835 B.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Split-by-face reuses the existing Cut feature rather than adding a new type:
two appended fields (cut_face_body, cut_face) let apply_cut derive the cut
plane from a picked face via SketchPlane::from_face when cut_face >= 0,
otherwise it keeps using the base `plane`. cut_offset / cut_flip still apply
along the derived normal, so the same square-wire split machinery handles
both cases. add_split_by_face() is the convenience entry point; MCP gains a
`split` method (body / face_body / face / keep_upper / keep_lower).
Serialization stays append-only — cut_face_body, cut_face appended to
save/load, recipe version unchanged at 2.
Tests: the previously-missing both-halves plane cut (keep_upper && keep_lower
=> two bodies whose volumes sum to the original), split-by-face via a
top-face plane offset into the interior, keep-upper-only, and a round-trip.
Golden fixture regenerated with a GoldenSplit cut-by-face feature and exact
field-value assertions. Suite 63 -> 67 cases, 1119 -> 1178 assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Pick a face of an existing body, offset it by a wall thickness along its
normal, and append the resulting thin solid as a new body. Onshape-parity
Tier-2 item; the kernel had Shell (hollow a whole solid) but no way to turn
a single face into a plate.
Kernel: CadFeatureType::Thicken, add_thicken()/apply_thicken() as a
body-level op next to Transform/Mirror. The picked face is wrapped in a
TopoDS_Shell and offset via BRepOffsetAPI_MakeThickSolid::MakeThickSolidBySimple;
the result is orientation-normalised to positive volume (same convention as
apply_mirror). Serialization stays append-only — thicken_face,
thicken_thickness, thicken_flip appended to save/load, recipe version
unchanged at 2.
MCP: `thicken` method (body/face/thickness/flip) plus the missing
feature_type_name() case.
Tests: 6 new [CadDocument] cases (plate volume within 1%, flip direction,
bad face id, zero thickness, fuse-with-source, round-trip). Golden fixture
regenerated with a GoldenThicken feature and exact field-value assertions.
Suite 57 -> 63 cases, 1054 -> 1119 assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Until now move and rotate lived only in the GUI as m_body_xform, a display
transform. That made them a correctness hole, not a missing tool: a moved body
recomputed and booleaned at its ORIGINAL position, and the move was not in the
recipe at all, so it vanished on save/reload. Only export_step consulted the
transform, which is why the discrepancy stayed hidden.
CadFeatureType::Transform makes it a real feature: rotate angle_deg about
xf_axis through xf_pivot, then translate by xf_translate, applied to the target
body with BRepBuilderAPI_Transform. xf_copy=true keeps the source and appends
the transformed body instead of mutating in place, which covers Onshape's
Transform/copy in the same feature.
Rotation is composed before translation (trsf = tr * rot) so the pivot means
what a user expects — the point the body turns about, not a point that then
drifts with the translation. A rotation with a degenerate axis is refused
rather than silently skipped; a zero angle skips the rotation entirely so a
pure move needs no axis at all.
The decisive test is not the bbox arithmetic but "moved body participates in a
later boolean at its new position": two coincident boxes, one moved to partial
overlap, fused. The fused volume must be strictly greater than one box (the
move took effect in the kernel) and strictly less than both (they still
intersect). With a display-only transform the first assertion fails.
Serialization stays append-only; recipe version unchanged at 2. Golden fixture
regenerated with a GoldenTransform feature carrying distinctive literals so a
field reorder shows up as obviously wrong values. Also fills in the Helix arm
of feature_type_name(), missing since the helix commit.
Kernel suite 51 -> 57 cases, 985 -> 1054 assertions, green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Adds CadFeatureType::Helix — the missing input for Sweep. Sweep already
existed but could only follow a sketch, so springs, coils, augers and
non-standard-pitch threads were unreachable. Helix + the existing Sweep now
gives all of them with no further work.
Built the OCCT way: a 2D line on a Geom_CylindricalSurface (Geom_ConicalSurface
when helix_taper_deg != 0) turned into an edge and lifted to 3D with
BRepLib::BuildCurves3d — a true analytic helix, not a sampled polyline, so a
swept spring is smooth rather than faceted. The axis is the plane normal
through the plane origin, matching how Revolve and Plane already work.
Sweep's path resolution is widened to accept either a Sketch (unchanged
behaviour) or a Helix, and rejects anything else with a clear error. Helix
itself is skipped in route_feature and recompute — like the datum features, it
produces no body and exists to be consumed.
Invalid input is refused rather than approximated: non-positive radius or
pitch, negative height, a turn count above 10000 (which would hang OCCT), and
a taper that would drive the radius negative before reaching the top all fail
with a specific error.
Serialization: helix_radius/pitch/height/left_handed/taper_deg appended at the
very end of both save and load, identical order, after the coordsys block.
SNAPORCA_CAD_RECIPE_VERSION stays 2; Helix is appended to the end of
CadFeatureType. Golden fixture regenerated with distinctive literals and
field-value assertions; all pre-existing assertions pass unchanged.
Tests assert analytic values. The one that actually proves it is a helix and
not a circle or a spiral: arc length of r=5 pitch=2 height=10 measured with
BRepGProp::LinearProperties against 5*sqrt((2*pi*5)^2 + 2^2), WithinRel 1e-3.
Plus bounding box (2r in X and Y, height in Z), the conical top radius, the
left-handed winding compared at equal parameter, and the integration test:
a circle r=1.5 swept along a 5-turn helix gives one valid solid of ~1115 mm^3
(WithinRel 0.1 — pipe sweeping is not exact).
MCP: `helix` method registered in describe_tools().
Ported from snaporca 6cdc6b5459. Two fork-specific adjustments: the two new
error-message assertions use Catch2 v3's ContainsSubstring (v2's Contains does
not exist here), and the golden fixture is copied rather than regenerated
because this fork cannot be compiled locally — make_golden_doc_v1() is
byte-identical across both forks, so the blob is provably the same.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Adds CadFeatureType::Axis and ::CoordSys — reference geometry that produces
no solid, modelled on the existing Plane datum feature.
Axis construction methods (AxisType): TwoPoints, FaceNormal,
CylinderCenterline, PlaneIntersection, AlongEdge. The centreline case is the
useful one: it gives a real axis through an existing hole or boss.
Coordinate systems (CoordSysType): PointWorld and FaceAndDirection. The
latter Gram-Schmidts the picked references, so the stored frame is
orthonormal even when the user's X hint is not perpendicular to the face
normal; the third axis is derived by cross product rather than stored, so it
cannot drift out of sync.
Revolve and pattern are deliberately NOT rewired to consume these — this
commit adds the reference geometry only and changes no existing behaviour.
Serialization: all axis_*/coordsys_* fields appended at the very end of both
CadFeature::save and load, identical order, after mirror_keep_original.
SNAPORCA_CAD_RECIPE_VERSION stays 2; Axis and CoordSys are appended to the
end of CadFeatureType so existing type ordinals are unchanged. Golden fixture
regenerated with distinctive non-default literals and field-value assertions
for every new field; all pre-existing assertions pass unchanged.
Tests assert analytic values: two-point axis direction exactly +Z with unit
length, cylinder centreline collinear with Z and on the true axis, parallel
planes fail cleanly, and the Gram-Schmidt frame is orthonormal to 1e-9 with
X x Y == Z. Degenerate input (identical points) fails with a non-empty error
rather than producing NaNs.
MCP: `axis` and `coordsys` methods registered in describe_tools().
Ported from snaporca 242d4efecb. The golden fixture is copied rather than
regenerated because this fork cannot be compiled locally (Eigen 5.0.1 vs the
build image's 3.3); make_golden_doc_v1() is byte-identical across both forks,
so the two fixtures are provably the same blob.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Adds CadFeatureType::Mirror: reflect a target body about a plane using
gp_Trsf::SetMirror + BRepBuilderAPI_Transform. BooleanMode::New keeps the
mirrored copy as its own body (mirror_keep_original decides whether the
source survives); BooleanMode::Add fuses it back into the source, so an
overlapping mirror does not double-count volume.
Serialization: mirror_keep_original is appended at the very end of both
CadFeature::save and load (append-only contract). The mirror plane reuses
the existing `plane` member and the body selector reuses `target_body`,
as Cut already does. Golden fixture regenerated at the current
SNAPORCA_CAD_RECIPE_VERSION = 2; the existing field-value assertions all
still pass unchanged, and the reorder tripwire was re-verified after
regeneration (swapping draft_face/draft_angle in `load` alone still
fails the golden test).
Tests assert analytic values: mirrored volumes equal (8000 each) with the
reflected centroid, Add on a non-overlapping asymmetric body gives exactly
2x volume, Add across an intersecting plane gives strictly less than 2x,
and an invalid body index fails cleanly with a non-empty error.
MCP: `mirror` method registered in describe_tools().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
# Description
Adding optional flag for enabling console logging introduced in #14439.
Console logging not working with LLDB-DAP on VSCode as per #14897
Code changes won't fix this because it is a LLDB-DAP issue documented in
#14909
Use flag `-DUSE_SLIC3R_CONSOLE_LOG=ON` or `-DUSE_SLIC3R_CONSOLE_LOG=OFF`
to enable/disable it.
Alternatively in you VSCode's settings.json,
```
"cmake.configureSettings": {
"USE_SLIC3R_CONSOLE_LOG": "OFF"
}
```
<!--
> 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)
# Description
Remove DEPENDS/empty-COMMAND args that are invalid in the
add_custom_command(TARGET) form (CMP0175), fix the FindDraco.cmake case
mismatch, and opt Boost lookup into upstream BoostConfig via CMP0167 for
the OpenVDB module and the CGAL find.
# 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.
-->
<!--
> 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)
Port of snaporca c2821af783. New GeometryEngine::mass_properties over BRepGProp
(separate VolumeProperties/SurfaceProperties), bounds-checked
CadDocument::body_mass_properties, and a mass_properties MCP method. Query-only:
no CadFeature, no serialization, no version change. Analytic tests
(WithinRel/WithinAbs): cube 8000/2400/COM(0,0,10)/inertia 533333, cylinder
500pi/300pi, hollow = solid-500pi, invalid index -> valid=false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Port of snaporca 04c3d579a7. Bump SNAPORCA_CAD_RECIPE_VERSION 1 -> 2;
deserialize_recipe sets a user-facing error distinguishing too-new / too-old
/ corrupt instead of a silent bare false. No per-version migration by design.
Golden fixture regenerated at v2 (v1 retired), field-value reorder tripwire
unchanged. Fork adjustment: Catch2 v3 string matcher ContainsSubstring
(not v2's Contains) in the two new version-mismatch tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Remove DEPENDS/empty-COMMAND args that are invalid in the
add_custom_command(TARGET) form (CMP0175), fix the FindDraco.cmake case
mismatch, and opt Boost lookup into upstream BoostConfig via CMP0167 for
the OpenVDB module and the CGAL find.
# Description
attempt to fix#14851
# 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.
-->
<!--
> 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)
CadFeature::save/load is append-only by contract, and ~20 planned features
each append fields. The existing roundtrip test cannot police that: it writes
and reads with the same code, so any self-consistent ordering passes. Only a
blob written by older code and stored on disk can detect that the format moved.
The first attempt at this test passed while the defect was present. I proved
it by swapping draft_face (int) with draft_angle (double) in both save() and
load() -- a genuine byte-layout change -- and it still reported 613 assertions,
exit 0. It asserted only derived geometry: body count, per-body volume, feature
types. The golden document had no Draft feature, so those fields sat at their
defaults, the reorder scrambled values nothing read, and the recomputed solids
came out byte-identical.
So the fixture now asserts the DATA, not what the data produces:
- make_golden_doc_v1() builds 22 features across 14 types (Draft, Shell,
Revolve, Pattern, Cut, Hole, Chamfer, Fillet, Extrude taper/symmetric,
Thread, Sweep, Loft, Boolean, Plane) with distinctive non-default literals
(draft_angle 7.25, shell_thickness 1.375, revolve_angle 217, pattern_count 5)
so a reorder produces visibly wrong values rather than swapped defaults.
- Layer 1 reads the committed blob with raw cereal and asserts field by field,
independent of recompute, so a geometry regression cannot mask a format break.
- Layer 2 keeps the geometry checks as a separate concern.
Verified to trip, twice, by deliberate breakage rather than by assertion:
draft_face <-> draft_angle -> draft_face reads 1075642368 (0x401d0000),
the high half of double 7.25
revolve_angle <-> revolve_axis -> revolve_angle reads 0.0, not 217.0
Both revert clean to 758 assertions / 30 cases.
Also scoped the "regenerate the fixture" hint to the feature-count check only.
It was in scope for every assertion in the block, so a detected reorder told
you to run [.regen] -- which would bake the corrupted layout in as the new
golden and permanently disarm the test. A guard must not advise disabling
itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Ported from snaporca f9e0f99bcb. The patch needed fuzz: this fork's copy of
test_caddocument.cpp carries a Catch2 v3 include, a `using Catch::Approx`, and
an extra [Deviation] case appended after line 1611 -- exactly where these hunks
land. Verified after applying: new symbols present, the [Deviation] case
intact, braces balanced, and only WithinAbs/WithinRel used (both exist in v3).
Compile and test verification here is CI, not local: this fork needs Eigen
5.0.1 while the local deps image ships 3.3.
Adds scripts/kernel-test.sh: build the libslic3r_tests target and run the CAD
kernel tags, exit code as the whole contract. Clean build 4m36s, incremental
18s, no display needed -- every [CadDocument] case builds a CadDocument,
recompute()s it and asserts on geometry.
Four things this had to get right, each found by it going wrong first:
- SLIC3R_GTK=3 and BUILD_TESTS=ON are mandatory. src/CMakeLists.txt turns
SLIC3R_GTK into 'wx-config --toolkit=gtk<N>', so omitting it asks for
toolkit "gtk", nothing matches, and configure dies with the thoroughly
misleading "Could NOT find wxWidgets" -- while wx-config sits right there
in the deps prefix, working. BUILD_TESTS=ON is what creates the target.
- Configure runs unconditionally. Guarding on "CMakeCache.txt exists" is
wrong because a FAILED configure writes that file too, after which the
guard skips reconfiguring forever and every later run silently reuses the
poisoned cache, ignoring corrected flags.
- A new build volume is always built clean. Cloning a warm cache from
another tree is a correctness trap: rsync preserves source mtimes, ninja
compares them against foreign object timestamps, concludes everything is
current and relinks stale objects. That produced a binary containing NO
[CadDocument] tests at all -- while exiting 0. A green run that tests
nothing is worse than a red one.
- --host builds where the deps image already lives, staged per volume so
parallel workers never share a tree.
The two [known-broken] tags: "entity constraints: tangent/midpoint/symmetric/
angle" aborts inside the vendored solver (slvs/dsc.h FindById, "Cannot find
handle"), and SIGABRT is fatal to the Catch2 process -- that single case took
the suite down at 12 of 31, so a green baseline was unreachable. The thread
groove case is a plain pre-existing assertion failure. Both are excluded from
the dev loop's default filter ONLY; ctest in CI still runs and reports them,
so neither bug is hidden. Baseline is now 603 assertions / 29 cases green,
which is what makes "I broke nothing" a meaningful statement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Enumerated from the source rather than from recollection: CadFeatureType and
add_* in CadDocument.hpp, Tool in DesignPanel.hpp, Mode in
DesignSketchTool.hpp, SketchConstraintType + SketchEntity::Type in
SketchEngine.hpp, and the JSON-RPC dispatch in McpControl.cpp.
Findings worth stating up front:
- The 2D sketcher is at or near Onshape parity -- 19 constraints, every entity
type including B-splines and elliptical arcs, trim/extend/offset/mirror and
both array kinds. Very little is missing there.
- The gaps are all breadth beyond sketching: assemblies/mates, surface
modelling, sheet metal, drawings, and variables/configurations.
- The most defensible criticism is the absence of variables and expressions.
Every dimension is a literal double, so the feature tree is parametric in
structure but not in value -- "change one number and the model updates" is
only half delivered. It is also the cheapest Tier 1 item to close.
The doc separates platform capabilities (version control, FeatureScript, FEA,
rendering, cloud PDM) into their own tier rather than counting them as missing
tools: that is Onshape-the-platform, not Onshape-the-modeller, and holding a
slicer tab to it would not be a fair comparison.
One entry is a correctness gap rather than a missing feature: move/rotate body
(m_body_xform) is display-only and never enters the B-rep, so a moved body
exports and booleans at its original position while the viewport shows it
moved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
# Description
Plugin discovery now distinguishes an unavailable or invalid
.install_state.json from a valid sidecar. During transient filesystem
replacement, the existing enabled state is preserved instead of being
reset, preventing plugins from unexpectedly losing their auto-load
behavior during rescans.
Thanks @WeLizard for pointing this out.
<!--
> 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)
# Description
Fixes some bugs reported by users and the community
# Fix 1: Latest Version
If the user updates the plugin version on OrcaCloud without a changelog,
the latest version shown on OrcaSlicer won't be accurate.
## Issue
OrcaSlicer uses the changelog returned by OrcaCloud backend as the
source of truth, even though we query a latest version from the backend
as well. The changelog only consists of version entries that have
changelogs so if the latest one, e.g. 1.2.0 has no changelog, but 1.1.0
has, the latest version will be interpreted as 1.1.0.
## Fix
Don't use the changelog for version tracking, just use it for the
plugins dialog changelog tab.
# Fix 2: Host theme script / window.orca bridge leaks into cross-origin
child iframes
If a plugin's embedded app renders its own `<iframe>` (its real
catalog/dashboard UI, or a third-party auth/payment widget), that child
document also gets, uninvited: the host theme `<style>` block fighting
whatever CSS the child page already defines.
## Issue
OrcaSlicer injects host themed scripts and window.orca bridge into every
page, potentially breaking any cross origin child frames or `<iframes>`
in general.
## Fix
Only inject top level frames by checking `if (window.top !==
window.self) return;`.
# Fix 3: Transferring Slicing Pipeline Plugin config when switching
printers (#14832)
Select an entry in the Process tab's "Slicing Pipeline Plugin" picker,
switch the active printer preset, then click **Transfer** in the
"modified settings" dialog — OrcaSlicer crashes immediately and
repeatably. Changing other settings (e.g. a single scalar option) does
not reproduce it; only this picker does.
## Issue
`slicing_pipeline_plugin` is a vector option (`coStrings`) with an empty
default. `deep_diff` diffs vector options per-index, so selecting a
plugin produced a `"slicing_pipeline_plugin#0"` dirty key instead of a
plain one. Unlike genuine per-extruder options, this key wasn't caught
by the printer-switch filter that discards stale per-extruder changes,
so it got cached and replayed through `ConfigBase::apply_only`'s
`'#'`-indexed branch, which calls `ConfigOptionVector::set_at()` on the
freshly-reloaded (and still empty) destination vector. `set_at()`'s only
empty-vector guard is an `assert()`, which is compiled out of Release
builds, so it dereferences `values.front()` on an empty vector —
undefined behavior, matching the reported ACCESS_VIOLATION.
## Fix
Treat `slicing_pipeline_plugin` as a single atomic value in `deep_diff`
(`Preset.cpp`), same as `printable_area`/`thumbnails`/etc.,
since it isn't actually per-extruder data. It's now diffed and replayed
as a whole option (`ConfigOptionVector::set()`, a plain vector
assignment) instead of the index-based `set_at()` path — removing the
crash unconditionally, regardless of whether the two printers share the
same extruder configuration.
# Fix 4: Stale .whl cache
After a .whl was loaded once, if at runtime, the .whl is replaced with a
new one, the plugin system will use the stale .whl cache.
## Fix
Added an option in the context menu to Reload or Delete Cache and Reload
for locally installed plugins. The assumption here is that users
shouldn't be modify cloud plugins, and if they want to develop on a
subscribed cloud plugin, they should create a local copy of it.
<!--
> 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)
The 2026-06-21 assessment was written before the persistence work landed and
got two load-bearing facts wrong. Both are corrected here against the branch
itself rather than from recollection:
1. It called OCCT "a dependency mainline OrcaSlicer has never carried" and
built its whole conclusion on that. False: deps/OCCT/ exists at the
merge-base, and upstream already links it from Format/STEP.cpp,
Format/svg.cpp and Shape/TextShape.cpp. The real dependency diff is one
line -- BUILD_MODULE_ModelingAlgorithms OFF -> ON -- costing a measured
3.77 MiB of Windows DLLs (TKFillet + TKOffset; TKBool already arrives
transitively via DataExchange).
2. It described the vendored SolveSpace solver as LGPL. False:
src/libslic3r/slvs/LICENSE is GPL-3.0. Harmless for us, but a licence
must not be misstated in a document aimed at upstream.
It also claimed no changes to Model, which stopped being true when 3MF
recipe persistence added a std::string there.
The rewrite replaces prose estimates with counted figures: 138 new files,
23 modified upstream files at +457/-75, nothing deleted, 99.3 % of the diff
in new files. That reframes the ask from "adopt a CAD kernel" to "widen a
build flag you already carry", which is the argument that actually has a
chance upstream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Selection failures on a real desktop kept looking identical from the UI
whether the ray missed the solid, the click was rejected as a drag, or the
press never reached the tool at all. The status line added in edb1adbfa3
reports WHICH body/face was picked, so it distinguishes "picked" from
"silence" and nothing finer -- not enough to tell those three apart.
This narrates the whole press -> release -> ray path on stderr, one distinct
line per failure mode:
down x= y= the press reached the tool
up with no pending press the press was eaten upstream
up ... drift=N / rejected the click-vs-drag threshold decided
ray tris=N -> body= face= t= the ray reached the solid (or missed)
no solid data (bodies= mesh=) the pick pointers were never wired
Gated on getenv("SNAPORCA_PICK_TRACE"), cached in a function-local static,
so a normal build pays one load and prints nothing. It ships enabled-on-
demand because the failing environment is a real X session with a real
mouse, which the headless rig cannot reproduce.
First use retired a wrong theory of my own: the trace showed drift=0 on
three consecutive picks, so the widened threshold in edb1adbfa3 was not
what fixed anything. The reported symptom is best explained by a stale
pre-orcawidgets binary left running alongside the current one.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
## Problem
On H2C (carousel) printers, the wipe tower purge volume calculation in
`_make_wipe_tower()` tracks filament state **per-extruder** (2 slots).
Since H2C has up to 7 carousel nozzle slots on a single extruder, all
filaments sharing that extruder are collapsed into one tracking slot.
This causes:
Test:
[5cubes.3mf.zip](https://github.com/user-attachments/files/30092551/5cubes.3mf.zip)
- **Massive redundant AMS flushing** every filament change on the
carousel triggers a full purge against the "previous" filament, even
when the target nozzle slot already has the correct filament loaded
- **60.9g total weight** instead of ~17g (**3.5× material waste**)
- **3h09m print time** instead of ~1h57m (**60% longer**)
## Root Cause
The code uses `nozzle_cur_filament_ids[extruder_id]` (a 2-element array)
to track which filament was last used for each extruder. BambuStudio
uses `NozzleStatusRecorder`, which tracks per `group_id` (physical
carousel slot 0..6).
## Changes
| File | Change |
|---|---|
| `Print.cpp` | Replace `nozzle_cur_filament_ids` with
`NozzleStatusRecorder`. Use `get_nozzle_for_filament()` to resolve the
physical carousel slot per layer. Select `filament_prime_volume_nc` for
nozzle changes, `filament_prime_volume` for filament changes. |
| `PrintConfig.hpp` | Add `ConfigOptionFloats filament_prime_volume`
(per-filament EC prime volume, missing from upstream but present in BBS
and H2C profiles) |
| `PrintConfig.cpp` | Register `filament_prime_volume` with default
45mm³ (matching BBS) |
| `Preset.cpp` | Add `filament_prime_volume` to preset keys |
Also includes `tests/compare_analyzer/` - two standalone Python tools
for G-code slice comparison and temperature timeline analysis (stdlib
only, no dependencies).
## Test Results (5-color H2C Hybrid print, same 3mf project)
| Metric | Upstream (broken) | **Fixed** | BBS (reference) |
|---|---|---|---|
| **Total weight** | 60.90g | **16.20g** ✅ | 17.47g |
| **Print time** | 3h09m | **1h57m** ✅ | 1h51m |
| **Filament changes** | 105 | 105 | 140 |
| **Tool changes** | 35 | 35 | 35 |
| **Critical discrepancies vs BBS** | ⚠️ YES | ✅ None | — |
## Analysis Tools (`tests/compare_analyzer/`)
Two standalone Python tools (stdlib only, no dependencies) for deep
G-code comparison:
- **`compare_slices.py`** - comprehensive .3mf slice comparison:
filament usage, nozzle mapping, tool change sequences, prime tower
analysis, temperature timeline, retract parameters, and automatic
critical discrepancy detection (weight/time anomalies)
- **`show_temp_plot.py`** - interactive HTML temperature timeline
plotter for visualising heater profiles during multi-nozzle prints
(supports single-file and side-by-side comparison)
Usage:
```bash
python3 tests/compare_analyzer/compare_slices.py file1.3mf file2.3mf --labels "Upstream" "Fixed"
python3 tests/compare_analyzer/show_temp_plot.py file1.3mf file2.3mf
```
## Screenshots
### OrcaSlicer Upstream (unfixed) - 60.90g, 3h09m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 22 58"
src="https://github.com/user-attachments/assets/3efb2bff-ff1e-43db-9669-feafa5921b51"
/>
### OrcaSlicer Fixed - 16.20g, 1h57m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 23 08"
src="https://github.com/user-attachments/assets/c1d36dc5-9b01-4691-80ef-7364540e1f4e"
/>
### BambuStudio Reference - 17.47g, 1h51m
<img width="1512" height="982" alt="Screenshot 2026-07-16 at 15 24 52"
src="https://github.com/user-attachments/assets/5c0b11e2-64f4-40e9-8c7c-3f42519786c3"
/>
### Temperature Timeline: Upstream vs Fixed
<img width="1511" height="829" alt="Screenshot 2026-07-16 at 15 23 35"
src="https://github.com/user-attachments/assets/926c2cb5-dfd4-4ce0-bb40-82e6165eb134"
/>
### Temperature Timeline: Fixed vs BBS
<img width="1512" height="825" alt="Screenshot 2026-07-16 at 15 23 51"
src="https://github.com/user-attachments/assets/85c72dda-e779-4aa6-8118-fb17e5f8482d"
/>
## Compatibility
Safe for non-carousel printers: when each extruder has a single nozzle,
`group_id == extruder_id`, so `NozzleStatusRecorder` behaves identically
to the original per-extruder tracking. The `filament_prime_volume`
default (45mm³) matches the existing global `prime_volume` default.
## Reference
BambuStudio `Print.cpp` `_make_wipe_tower()` L3341-3392 -
`NozzleStatusRecorder` pattern.
* Ensure spiral lift positive quadrant
* check for printable area
* clamp area
* simpler version
* Adjust printable area bounds with safety safety margin
Added safety margin to printable area bounds calculations.
* increase safety margin
* Refactor safety margin calculations in GCodeWriter
* New Logic
Co-Authored-By: Ian Bassi <12130714+ianalexis@users.noreply.github.com>
---------
Co-authored-by: Ian Bassi <12130714+ianalexis@users.noreply.github.com>
# Description
Typo introduced a regression failure. Fixes failing 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)
Adopt BambuStudio's load_last_machine (cloud machines only, remembered
machine preferred) and port record_user_last_machine /
get_user_last_machine. Orca's version auto-connected an arbitrary LAN
printer, starting an unrequested connection that the user's first
printer switch tore down mid-flight, which the 02.08 plugin's connect
worker does not survive. This also caused the 100% crash-on-relaunch
loop: the crashed-on printer was auto-connected at startup, so
re-selecting it always hit the same-machine disconnect+reconnect path.
* fix: constrain PA_Line calibration bounding box to model geometry
- PA_Line bounding box now uses actual model convex hull for X constraint instead of full printable area
- Skip EXCLUDE_OBJECT_DEFINE for PA_Line mode (single object, no exclusion needed)
- Add tool config files to .gitignore
* modified: .gitignore
* fix: add gcode type annotations and fix box height in PA line calibration
- Label calibration segments with appropriate TYPE comments (Outer wall, Bottom surface, Top surface, Custom) for proper gcode processing
- Fix bounding box height calculation to account for z_offset
- Add LAYER_CHANGE and HEIGHT comments for multi-layer numbering display
* fix: lock PA_Line bounding box X to bed centre instead of model hull
* Added extra TYPE to ensure text/numbers/glyphs are labelled correctly
` gcode << ";TYPE:Outer wall\n"; `
* Remove hardcoded Perl path in OpenSSL.cmake
Remove hardcoded Perl path for Windows configuration.
* Add cross compilation support for Windows in OpenSSL.cmake
* Remove unnecessary blank line in OpenSSL.cmake
* Set perl config command back to variable
Accidentally uploaded version with hardcoded path for my local environment
* whitespace adjustment in previous
* removed personal .gitignore config
Remove specific files and directories from .gitignore.
* Fix box height parameter in DrawBoxOptArgs
Update DrawBoxOptArgs to use `m_height_layer` instead of `m_height_layer*2+z_offset`. This isn't a Z coordinate, it's a layer height
* Replace hardcoded extrusion type with call to `GCodeProcessor::reserved_tag` Etags
* Get pa_line bounding box from actual calibration variables
Updated calibration line bounding box calculation to use actual geometry for X bounds, using a fake call to generate the calibration pattern.
This will accurately get the size of the calibration pattern, massively reducing wasted time from bed mesh probing.
* Implement print_extents method in CalibPressureAdvanceLine
Add print_extents method to calculate bounding box extents based on bed dimensions.
* Declare print_extents method in CalibPressureAdvanceLine
Added print_extents method to return X-bounds of the pattern.
* Fixed whitespace issues
* Adjust print_extents to account for delta printers
Check if the printer is delta layout and adjust bed dimensions if so.
Used code from `CalibPressureAdvanceLine::generate_test`
* Added semicolons to reserved tags
Didn't realise etags wouldn't add semicolon - added these
* only include number list in bounding box if number list is used
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* avoid bounding box overflowing bed
* Tabs to spaces
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Ports snaporca-cad f5c7e74e9b.
The LeftUp pick discarded anything moving more than 4 px total since the
press. A hand-held mouse drifts that much during an ordinary click, so
real clicks were thrown away as drags and it read as "selection does not
work". Use 8 px per axis, GTK's own drag threshold.
Also report the pick on the status line (body / face / edge). A solid
pick previously set no text at all, so its only feedback was the viewport
highlight, and a pick that registers but draws faintly looked identical
to one that never fired.
DesignSketchTool.cpp copied verbatim (identical between the forks apart
from this change). DesignPanel.cpp took the status hunk only, since this
fork keeps mainline's Item-based DropDown in feat_dropdown/ToolFlyout.
Not confirmed on hardware yet, on either fork; this fork remains
uncompiled (needs Eigen 5.0.1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
- WipeTower: use filament_ramming_volumetric_speed(_nc) for ramming, falling back to
max_vol_speed only when nil; gate precool temps on enable_pre_heating
- ToolOrderUtils: disable the inter-layer forecast in the per-nozzle base reorder so
H2D/H2C ordering is unchanged
- PrintConfig: stop stripping filament_prime_volume in handle_legacy; document that
prime_volume drives the Type2 wipe tower and filament_prime_volume the Type1 one
- GCodeProcessor: exclude post-print end-gcode M400 dwells from the M73 estimate and
drop the dead air-filtration state
- Trim verbose BambuStudio source-location comments across the port
Ports snaporca-cad c741fb9677.
SketchInlineEditor::open() clamped its position with
wxGetClientDisplayRect(), which describes only the PRIMARY monitor. On a
multi-head desktop (the reporting machine runs 5760x1080 across screens
at +0, +1920 and +3840) a field anchored on the left or right screen was
clamped onto the middle one and left invisible, while m_awaiting_length
made the sketch tool consume every mouse event until it was answered:
orbit and pan died after any sketch, with Enter the only way out. Clamp
to the display the anchor is actually on instead.
Also let drags and the wheel through that freeze, so a field that lands
somewhere unexpected degrades to odd placement rather than a dead
viewport.
Both files were byte-identical between the forks apart from this change,
so they are copied verbatim. Verified on the snaporca side by the user;
this fork is still uncompiled (needs Eigen 5.0.1, which the available
deps image does not provide).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
# Description
<!--
> 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.
-->
load_local_machines_from_config() iterated a reference to the live
m_local_machines map while erase_local_machine() erased from it for
printers without access rights, invalidating the range-for iterator
(use-after-free on ++it). Iterate a copy instead, as the code did
before commit 5028a5000e.
Mirrors snaporca-cad a016dffb48 + 4b828c4798 so the two forks do not drift:
- Sidebar on Orca's widget set: ComboBox for all 25 pickers, StaticBox
frames for the tool cards / feature tree / bodies list, framed double
spins, and no stack of full-width action buttons (Prepare's left panel
is parameters only).
- Toolbar: document group + undo/redo + mode-gated tools, commit far
right, ordered via keyed slots, at Prepare's 40 px / 4 px geometry.
- Polygon options and a new Move/Rotate card (distance, axis, angle) in
the sidebar instead of inline in the toolbar row.
- DesignSketchTool: stop consuming the LeftDown over a solid. Orca starts
a rotate drag on the press, so swallowing it killed orbit/pan whenever a
body was on screen. The pick now resolves on LeftUp within 4 px.
DesignSketchTool.{cpp,hpp} were byte-identical across the forks and are
copied verbatim. DesignPanel.cpp needed one hand-merge: mainline's
DropDown is Item-based (DropDown::Item{text,tip,icon}, DropDown(items&))
where snaporca passes parallel vectors, so feat_dropdown/ToolFlyout keep
the mainline form. Checked field-by-field against this fork's
Widgets/DropDown.hpp.
scripts/docker-iter-build.sh: mount the root CMakeLists.txt and cmake/
rather than inheriting the baked copies, which silently drops the
SLIC3R_CAD gate, and stop defaulting to snaporca's build volume - sharing
it made the two forks overwrite each other's cache and binary.
NOT COMPILED. This fork needs Eigen 5.0.1 (find_package(Eigen3 5.0.1
REQUIRED)) while the available snaporca-deps image supplies 3.3, so it
needs its own deps build to verify. The behaviour above was verified only
on the snaporca side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
* Call proper EXT variant of the function if framebuffer type is EXT
* Update glad with `GL_EXT_framebuffer_blit` and `GL_EXT_framebuffer_multisample` extensions support
Xcode's CodeSign phase rejects the bundled Python runtime's dotted dirs under Contents/MacOS. Disable it for the Xcode generator so the linker ad-hoc signs local dev builds; CI (Ninja) and the notarized bundle are unaffected.
Give previously-bare panels a keyed background so the existing dark-UI walk
can theme them. Runtime-created widgets (HMS items, device firmware/nozzle
panels) route their whole subtree through one UpdateDarkUIWin(this) call
instead of per-widget darkModeColorFor, which also themes their child text
and follows live light/dark switches. Extruder-card chips re-apply their
colours from Sidebar::sys_color_changed so a live switch updates them too.
Design drew raw OS wxCheckBoxes (grey square + inline text) where Prepare shows
Orca's teal check, one of the most visible reasons the two panels looked
unrelated. The six sidebar checkboxes (extrude flip, hole through, thread
internal, revolve flip, boolean keep-tool, loft ruled) now use Widgets/CheckBox:
the widget carries no text, so each label moves into the row's left column,
which is also Prepare's row idiom and matches the label/control grids.
CheckBox is a wxBitmapToggleButton, so its per-control Binds and the panel-wide
preview refresh listen for wxEVT_TOGGLEBUTTON as well; boolean/loft get a
label+control row instead of a bare full-width control. Checkboxes align to the
left edge of the control column, as Prepare aligns its own.
The two sketch-toolbar checkboxes are left alone — they live in the top toolbar,
not the left panel.
Verified on :10: Flip direction renders as a teal check with a white tick and
toggles correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Second half of the Prepare alignment. The 14 tool forms were plain 2-column
wxFlexGridSizers whose control column never grew, so every control sat at its
natural width right next to its label — nothing lined up, and it looked nothing
like Prepare's "label ......... [value]" rows.
- two_col_form() builds the grid with a growable control column; all 14 forms use
it and their 56 controls are added with wxEXPAND, so controls fill one aligned
column at the panel edge.
- The sketch-session Plane row is a box sizer, not a grid, so it gets a stretch
spacer for the same effect.
- The DoF readout collapses when empty, and the Bodies block starts hidden — both
reserved a blank line on a fresh document, leaving dead space above the action
buttons that Prepare does not have.
Verified on :10: Extrude edit shows Extrude dist / End / 2nd dist / Taper /
Result as aligned label+control rows; Plane row right-aligns; no Bodies box on an
empty document.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The two tabs used different idioms for the same concepts, which reads as two
different apps: Design was 264 px against Prepare's ~467 (the canvas edge jumped
on every tab switch), used bare micro-labels where Prepare uses icon + Head_14
card headers with a rule, hung its row actions on a loose strip under the tree
instead of in the section header, and drew raw OS-default wxButtons next to
Prepare's Orca-styled ones.
- Width now tracks Prepare's sidebar at runtime (sync_sidebar_width() reads the
live width on tab activation) rather than being hardcoded, so the two cannot
drift apart if Orca changes its sidebar.
- "Feature tree" and "Bodies" use the card_header() helper the panel already had
(icon + Label::Head_14) plus a wxStaticLine, exactly as the tool cards do.
- The six row actions moved into the Feature tree header, Prepare-style, at
header weight (24 px) instead of 36 px control weight.
- Buttons are Orca Buttons (ButtonType::Expanded, full width); Commit to Plate
gets ButtonStyle::Confirm as the tab's primary action.
- Margins/spacing come from SidebarProps (ContentMargin/TitlebarMargin/
ElementSpacing) instead of hardcoded 12/6/4.
Verified on :10: sidebars are the same width and share the header idiom.
Still to do: label-left/control-right rows inside the tool dialogs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Bodies were appended INSIDE the feature tree, after the features, and only when
there was more than one body. Two consequences:
- With a single body — sketch + extrude, the common case — no body row existed
at all, so the solid could not be selected from the tree. That also blocked
Move (it requires a selected body), the show/hide eye and every body-targeted
op; the viewport was the only way to select.
- With several features the Bodies group was pushed past the tree's auto-sized
height (capped at 9 rows) and clipped out of view, so bodies became
unreachable as history grew.
Bodies now live in their own list under the feature tree, mirroring Onshape's
Features + Parts split that the rest of the tab already follows. The list is
hidden while empty, sizes to its content (scrolls past 6), keeps the selected
row across a recompute, and greys hidden bodies as before. Selecting in either
list clears the other, so only one thing is ever "the target".
Verified on :10: single body -> Body 1 listed, selectable, Move opens the gizmo
on it (previously impossible); two imported solids -> both listed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
# Description
Support the 02.08.01 network plug-in and resync the Bambu device
workflow
Brings the Bambu network plug-in up to the 02.08.01 series and reworks
the plug-in
lifecycle around it (ABI gating, OTA install, hot reload), then catches
the device and
monitor side up to what that plug-in and current firmware expose — AMS
mapping, send-flow
checks, print options, and the device status pages.
**Included:**
_Network plug-in_
- Default to the 02.08.01 series and bind its new ABI symbols; bump the
reported
`SLIC3R_VERSION` to 02.08.01.55
- Gate loading to ABI-compatible `AA.BB.CC` series, and store plug-in
identity by series
rather than by exact build
- Fix the OTA update flow: install immediately, hot-reload without a
restart, and destroy
the agent before unloading the DLL
- Version selector lists same-series OTA builds newest first, with
dynamic
"(Latest)" / "(installed)" labels
- Restart printer discovery and reload saved LAN printers after a hot
reload
_Device layer_
- Move axis, calibration, chamber, status and upgrade handling into
DeviceCore; drop the
transitional shims
- Resync the DeviceCore state models, accessory firmware versions,
cold-pull state and
filament checks
_Device UI_
- Resync the AMS control/item widgets, device dialogs, device tab
widgets and status pages
- Add the AMS best-position popup, filament-change stop button, hub
version row and HMS
fallback
- Device error dialog: print-failure snapshot, purification and
don't-remind actions
_Send flow_
- Resync the send flow and port the remaining pre-send checks and
advisories
- Warn when a filament will switch extruders mid-print; block TPU on the
l
without firmware support (O1D/O1E)
- Map switch-bound AMS trays to both extruders and show combined nozzle
mapping
- Add the shared-PA-profile toggle
_Print options / timelapse_
- Restructure DevPrintOptions around a detection-option map and parse
the
bits on the live push path
- Add firmware print-option toggles, plus timelapse storage-location
selection with a
free-space check
_Fixes_
- Inverted MQTT jog on i3-architecture printers, plus assorted popup,
mapping and
status-parse defects
_Tests_
- New Catch2 coverage for network version selection and device filament
mapping
Printers that don't advertise the new capabilities are unaffected —
every added behavior is
gated on a firmware or plug-in capability flag, and the only profile
chang
`support_print_check_firmware_for_tpu_left` to O1D/O1E.
# Screenshots/Recordings/Graphs
<img width="714" height="500" alt="image"
src="https://github.com/user-attachments/assets/33e54581-3fb0-4a3e-b80c-afa8adfabe4c"
/>
<img width="530" height="212" alt="image"
src="https://github.com/user-attachments/assets/d0c55fd5-74c3-4aab-81a5-786d0aaf8828"
/>
<img width="483" height="527" alt="image"
src="https://github.com/user-attachments/assets/71ceaad1-a082-41cb-aa1a-4b9abb30d9db"
/>
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> 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)
The Design gizmo already had both move arrows and rotation rings, but drew them
at a fixed 70 px arm regardless of the body: on a 40 mm cube everything
collapsed into a ~100 px tangle buried inside the solid, so the rings were
effectively invisible and the tool read as "move only, no rotate".
Orca's Prepare gizmos size themselves from the selection's bounding sphere
(GLGizmoRotate3D: m_radius = Offset + sphere radius) so the handles always clear
the object. Same rule here: DesignPanel passes the body's bounding-sphere radius
(scale-aware) into the gizmo, and move_gizmo_arm() returns
max(70 px, 1.25 * radius) — the screen-space floor keeps it grabbable on a tiny
body or when zoomed far out. Ring radius and BOTH hit-tests derive from that one
helper, so picking cannot drift from what is drawn.
Verified on a 40 mm cube: rings now encircle the body, arrow drag moves with a
live mm readout, ring drag rotates live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Export wxWidgets include dirs and defines from libslic3r_gui on Linux so
tests outside src/ compile against its GUI headers. Relocate the bundled
Python runtime to Contents/Resources with a Contents/MacOS/python symlink
(codesign cannot seal the dotted python3.12 dirs under Contents/MacOS) and
replace the deprecated codesign --deep with explicit inside-out signing of
every Mach-O in the bundle.
The stored plug-in identity is now the AA.BB.CC series (matching BambuStudio)
rather than the full build version, so the meaningless 4th build digit stops
driving config, the whitelist, filenames, and the version selector. This fixes
the macOS "can't switch to an older build" bug: the cloud endpoint is
series-keyed and only ever serves a series' newest build, so downgrade-by-
download was impossible and the download silently adopted the latest.
A startup migration normalizes an existing full-version config and file name to
the series form with no re-download, the loader resolves a bare series to the
newest same-series build on disk, and user-provided custom-named plug-ins
(libbambu_networking_02.08.01_custom.*) are still enumerated and loaded.
Discovered builds were inserted positionally behind the whitelist entry they
anchored to, so an OTA-installed 02.08.01.53 listed below 02.08.01.52. The list
is now built by appending and sorting once, which also drops the per-entry
insertion scans.
"(installed)" tested whether the library was present on disk, so it marked every
version ever selected - switching leaves the previous library in place. The flag
is now is_loaded, resolved against the plug-in actually loaded, and the two
combo populators share one label helper.
restart_networking() rebuilds m_agent with a null printer agent, so the bare
m_agent->start_discovery() no-oped (NetworkAgent::start_discovery returns false when
m_printer_agent is null) and LAN discovery stayed dead until the user next changed a
preset. Call switch_printer_agent() instead - it installs the printer agent for the
active preset and then starts discovery, mirroring startup.
When the network plugin is not yet installed at startup, on_init_network builds
the DeviceManager without an agent, so its constructor skips loading the persisted
LAN printers. After the plugin is installed and the network stack hot-reloads, the
manager is reused via set_agent(), which never loaded them - so previously paired
printers stayed missing from the device list until an app restart. Load them once a
real agent first arrives.
- Allow mapping to EMPTY trays only from the multi-machine send page and keep
both panels pickable in the LEFT_AND_RIGHT view
- Guard the error-dialog cloud snapshot against stale callbacks and fall back
to the local illustration on timeout
- Parse the ipcam storage-check ack and axis/chamber pushes defensively
- Strip fan-control telemetry, initialize the upgrade error code, restore the
.json filter in the model-id scan
- Fix best-position popup tray lookup, gradient placement, and colour-list
ownership
- Cover switch binding sets and invalid-track transients in DevMapping tests
Trim the version whitelist to the latest series plus the pinned legacy build,
and reject out-of-series configured versions at startup, compatibility check,
and load failure - falling back to the latest installed build or the clean
re-download flow so the config never keeps pointing at an unsupported build.
* Block plugins from reading or writing app config and cloud credentials
Add a denied-filename registry to the plugin audit sandbox, seeded with OrcaSlicer's config (.conf/.ini) and the cloud refresh-token file. The deny is checked above the loading-mode read exemption and the allowed roots, so a plugin cannot reach these files even though they sit inside data_dir(), which is itself an allowed root. Case-insensitive prefix matching also covers the .bak/.tmp companions that hold the same data, and os.rename/os.remove are hooked alongside open so the files cannot be deleted or clobbered either.
# Introducing a Python Plugin System (WIP)
This PR opens up a way to extend OrcaSlicer with **Python plugins** —
small scripts (or full wheels) that run inside an embedded CPython
interpreter, without anyone having to fork the app or touch the C++
core.
I'm putting this up **early and on purpose**. It works end-to-end today,
but it is not finished and the public surface is deliberately small.
Before we lock anything in, we want the community's opinions on the
three decisions that are hard to reverse later: **what API we expose,
which plugin types we invest in, and how the security/audit layer should
behave.** Consider this a request for comments more than a merge
candidate.
## Why
People keep wanting to bolt their own behavior onto the slicer — custom
G-code post-processing, automation, bespoke printer/host integrations,
one-off analysis. Today that means maintaining a patched fork. The goal
here is a *sanctioned* extension path: a stable, documented seam where a
plugin can hook into a specific point in OrcaSlicer's workflow, with a
clear boundary around what plugin code is allowed to do.
## What's in this PR
**An embedded Python runtime.** A single CPython interpreter is started
once (intended to be on the main thread), with proper GIL handoff so
plugin code can run from worker threads. Plugin `stderr` (including
tracebacks from threads a plugin spawns) is persisted to
`data_dir()/log/python_*.log`.
**One API module, `orca`.** This is the surface a plugin sees. It
exposes the plugin base classes, the `@orca.plugin` decorator and
`register_capability()`, a typed `ExecutionResult`, and the
`PluginType`/`PluginResult` enums, along with per-type base classes
under `orca.gcode` / `orca.script` / `orca.printer_agent`. A host
bridge, `orca.host`, provides **read-only** access to the current model
and preset/config values, plus interactive `host.plater()` and `host.ui`
helpers (messages, dialogs, windows, progress). There is deliberately no
*write* access to slicer models or config, and no general GUI/toolkit
access beyond these host helpers. The exact shape of `orca.host` is one
of the things we most want feedback on.
**Three plugin types to start:**
- `post-processing` — runs during G-code export and receives the G-code
path + output context.
- `script` — a manual "Run" action from the Plugins dialog.
- `printer-connection` — a Python "printer agent" that registers into
the network layer on load. This is still WIP, along with a printer agent
workflow that is also WIP.
(The `PluginType` enum reserves several more names — Automation,
Analysis, Importer, Exporter, Visualization — but only the three above
are wired up.)
**Two packaging forms:** a single `.py` file with [PEP
723](https://peps.python.org/pep-0723/) inline metadata, or a `.whl`
wheel (with third-party dependencies installed via a bundled `uv`).
**Discovery, install, and a Plugins dialog** — local side-loading plus a
cloud subscription service, catalog/loader lifecycle, and per-plugin
error reporting in the UI.
**Audit-hook groundwork (PEP 578).** Every C++→Python call opens a
per-call audit context, and a CPython audit hook filters filesystem
access against a write allow-list (`data_dir()`, plus scoped roots like
the current G-code folder). This is *groundwork, not a sandbox* — see
Limitations.
**Docs.** Substantially complete author and contributor guides live
under `docs/plugins/` (development guide, security/audit deep-dive,
architecture overview, worked examples); the *feature* is what's WIP,
not the docs.
## Orca Cloud integration
Plugins are **fully integrated with Orca Cloud**, distributed in a
similar way to preset bundles — so this builds directly on the cloud
foundation rather than bolting on a separate mechanism.
- **Subscribe, don't side-load.** Instead of manually copying files, you
subscribe to a plugin from the cloud and OrcaSlicer pulls it down and
loads it for you — the same one-click experience as preset bundles.
- **Tied to your account, synced across machines.** Subscribed plugins
live under your user (`orca_plugins/_subscribed/<user_id>/`) and follow
you to any machine you're signed in on, exactly like your presets. Sign
out and the cloud plugins are unloaded; sign back in and they're
restored.
- **Stays up to date.** When a new version is published, OrcaSlicer can
fetch and install the update rather than leaving you on a stale copy.
- **Managed from the Plugins dialog.** Browse, install, update, and
unsubscribe live alongside local side-loading — which still works for
development and private plugins.
- **Integrated with presets.** Plugin references travel with a preset
bundle (via the preset's `plugins` fields), so publishing a preset that
uses plugins carries those references through the existing cloud sync.
If a referenced plugin is missing when you install the bundle on another
machine, OrcaSlicer **offers to install** the missing plugins for you (a
one-click prompt), provided those plugins are on the cloud.
## Where we need feedback
Really any form of feedback would be helpful; we'd rather grow this
slowly from real use cases than expose internals we can't keep stable —
which is why the current surface is kept small. The `orca.host` API in
particular is where we'd most value opinions.
## Limitations / known gaps (it's WIP)
- **The audit hook is not a sandbox.** It currently enforces only the
`open` event's writes, and only for string paths (fd/bytes opens are not
checked). `subprocess`, sockets, `ctypes`, `os.open`, and non-`open`
filesystem mutations (`os.remove`/`rename`/`mkdir`) are **not** blocked
yet. An `Enforcing` mode is stubbed but not yet wired, so today all
calls run in the writes-only "loading" mode. More details can be found
[here](https://www.orcaslicer.com/wiki/developer_reference/plugin_development/plugin_audit_hook.html#limitations).
- **The `orca` API is unstable** and will change based on this
discussion. Don't build anything load-bearing on it yet.
- The `requires-python` field is parsed but not enforced.
- Dependency install and some of the Plugins dialog UX are functional
but still rough around the edges.
## Docs
[How to
Use](https://www.orcaslicer.com/wiki/plugins/getting_started.html)
[Developer
Reference](https://www.orcaslicer.com/wiki/developer_reference/plugin_development/plugin_system.html)
## Software Development Kit
Currently, there is a script `generate_orca_python_stubs.py` to generate
the `.pyi` files that can be used for intellisense. We will release the
stub file as an SDK in future releases, but for now, if you intend to
develop plugins, you can generate the stub files locally.
## Notes
This system was developed primarily on Windows and Linux; testing on
macOS has so far been limited. macOS-specific behavior — the bundled
Python/`uv` runtime, path handling, and the audit hook — is the most
likely to need attention, and feedback or testing from macOS users is
especially welcome.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
## Orca Cloud to OrcaSlicer Plugins Workflow Overview:
https://github.com/user-attachments/assets/abbd7900-3062-4e33-8f77-5d30d567be1d
# Description
Use Space to trigger a new **speed dial**, which allows users to run
**app actions**. The only app actions implemented currently are python
plugin scripts.
## Notes
- Only toggleable in the Prepare (3D) view. Press Space, type to filter,
Enter or double-click to run.
- Focuses on search bar automatically
- Frecency-sorted (run count + recency) with alphabetical fallback (C++
computes)
- Pin actions as favourites and they will show on the top favourites
bar. Done through star icon on each row.
- Script plugins have no icon art yet, so tiles show a collision-aware
monogram: the capability's initial, escalating only when names collide -
prepend the package initial, then add an ordinal - so same-named actions
from different plugins stay distinguishable.
- E.g., capability Bravo from plugin Alpha normally shows just B. If
another action's name also starts with B, they disambiguate by
prepending the package initial (Alpha -> AB). If two still collide on
both initials (both AB), they become AB1 and AB2.
- "Run X?" confirm with a per-plugin "don't ask again" scope, owned
C++-side; suppression persists.
- Persistence (`speed_dial` AppConfig section): `favourite_actions`
(ordered id list) + per-action `stats` + `ask_suppressed`.
- Example of shape in data_dir:
```json
{
"speed_dial": {
"ask_suppressed": "[\"9b12aa079924bbc4\"]",
"favourite_actions": "[\"ccfdf8b9e492b624\",\"9b12aa079924bbc4\",\"b7abfa67626248e4\"]",
"stats": "{\"31d9d129a616a8b7\":{\"count\":4,\"last\":1783924989},\"53ec17d430634f62\":{\"count\":3,\"last\":1783939329},\"9b12aa079924bbc4\":{\"count\":5,\"last\":1783924981},\"9f2cb0d3ca56a87c\":{\"count\":1,\"last\":1783668370},\"b7abfa67626248e4\":{\"count\":4,\"last\":1783939325},\"f93469da14248128\":{\"count\":3,\"last\":1783939337}}"
},
}
```
# Screenshots/Recordings/Graphs
<img width="688" height="335" alt="image"
src="https://github.com/user-attachments/assets/683efa3b-9401-4977-a347-d70193188165"
/>
<img width="681" height="172" alt="image"
src="https://github.com/user-attachments/assets/fafb4965-054b-4fd5-ad6a-03145264fbe0"
/>
<img width="683" height="335" alt="image"
src="https://github.com/user-attachments/assets/00d5bd49-95c0-4f2f-966b-6c04c14c3cf3"
/>
## Tests
- **Web layer (green):** node-vm logic test `test-speeddial-logic.js`
covers `filterActions`, `visibleFavourites` (incl. the runnable guard),
`selectedActionId`, `resultCountText`, `actionLabel`, `tileCode`,
`nextSel`, and payload seeding - pure helpers, DOM-free.
- **Backend:** `ActionRegistry` FNV-1a id golden-vector Catch2 test
(`test_speed_dial_action_id`) pins the hash; the registry was verified
by fresh-context review including a threading fix (`run()` operates on a
stack copy so a queued refresh can't reallocate the action vector
mid-run).
- **Manual (all passing):**
- Space opens the dial in Prepare only; no regression to existing
Prepare-tab keys or the Plugins dialog.
- Search auto-focuses; typing filters live; a freshly-run action rises
in the frecency order.
- Up jumps to the favourites bar, Down into the list, Alt+1..9 hits
favourites; Enter and double-click run the highlighted action.
- Star pins/unpins an action; favourites persist across an app restart.
- "Run X?" confirm with per-plugin "don't ask again" is respected on
later runs.
- The `?` shortcuts dialog shows the Space row.
- Verified in both light and dark themes.
## Known Issues
When there are no actions, plugins, or scripts, the search bar will show
"Search 0 actions". This is bad UX. One alternative considered was to
show a call to action, for example "Please load plugin scripts so that
they appear here".
However, this is ultimately not implemented, as eventually it is not
expected that actions will be empty. The action registry will not be
expected to be empty because we will include in-app actions such as
opening dialogues or other app actions.
<img width="722" height="117" alt="image"
src="https://github.com/user-attachments/assets/a4b9c0db-b2bf-44bc-9550-398dd8b4c7aa"
/>
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
Adapt the speed dial's ActionRegistry to the collapsed
get_plugin_capability(PluginCapabilityId) overload, and restore the
script success/skipped status message the dialog lost when its
PluginScriptRunner refactor was superseded by ActionRegistry.
# Description
This PR introduces persistent, capability-scoped configuration for plugins. Users can configure plugins through the Plugins dialog instead of manually editing the plugin’s Python source.
Configuration exists at two levels:
- **Global** — one configuration per capability, shared by every preset. Edited in the Plugins dialog’s **Config** tab.
- **Per preset** — an optional override stored on a process or printer preset, edited from that preset’s **Plugin Preferences** group. A preset that overrides a capability configures the slices it drives; presets that do not simply use the global configuration.
Plugin authors can use the built-in JSON editor or provide a custom HTML settings interface. Both levels use the same editor and the same stored shape.
## New Plugin Configuration APIs
The following APIs are available to all plugin capability types.
### Python APIs
- `get_config() -> str` — Returns a raw JSON string of the capability’s effective configuration: the active preset’s override if it has one, otherwise the global configuration, otherwise `{}`.
- `save_config(config) -> bool` — Persists JSON-compatible configuration for the capability and returns whether the write succeeded. Always writes the **global** configuration (see [Preset overrides](#preset-overrides)).
- `get_config_version() -> str` — Returns the plugin version that last saved the configuration `get_config()` returned, from that same level, or `""` if it has never been saved.
- `has_config_ui() -> bool` — Override and return `True` to use a custom configuration interface instead of the built-in JSON editor.
- `get_config_ui() -> str` — Returns the HTML used to render the custom configuration interface.
- `get_default_config() -> str` — Returns a raw JSON string of the configuration applied by **Restore defaults** in the Plugins dialog. The default implementation returns `{}`.
### Custom UI JavaScript APIs
Custom configuration interfaces receive a sandboxed `window.orca` bridge:
- `window.orca.getConfig()` — Returns the current capability configuration.
- `window.orca.saveConfig(config)` — Requests that the host persist the supplied configuration.
- `window.orca.onConfig(callback)` — Immediately invokes the callback with the current configuration and invokes it again after successful saves or restores.
`saveConfig()` is asynchronous and does not return a Promise. Custom interfaces should use `onConfig()` to observe the successfully persisted state.
## Plugins Dialog
Every activated capability appears in the Plugins dialog’s **Config** tab, where its global configuration is edited.
The editor shown for a capability is selected as follows:
- If `has_config_ui()` returns `True` and `get_config_ui()` returns valid, non-empty HTML, the dialog renders the custom interface.
- Otherwise, the dialog renders the built-in JSON editor.
- If a custom interface cannot be loaded, the dialog reports the error and falls back to the JSON editor.
- The **Restore defaults** action replaces the stored configuration with the value returned by `get_default_config()`.
Custom interfaces run in a sandboxed iframe and can access configuration only through the provided `window.orca` bridge.
## Preset overrides
Process and printer presets gain a **Plugin Preferences → Capabilities** setting (Advanced mode). Its **Configure** button opens a dialog listing the capabilities that preset actually uses — the ones its `plugins` manifest declares *and* one of its plugin-backed options points at — and edits each one’s configuration for that preset alone. The button shows the number of overrides the preset carries.
That dialog offers two actions:
- **Save** — stores the edited configuration as this preset’s override.
- **Restore defaults** — discards the preset’s override, so the capability falls back to the global configuration. A preset holding no override *is* a preset at its defaults.
### How a running capability reads its configuration
`get_config()` resolves in this order:
1. The active preset’s override for this capability, if it has one.
2. The global configuration in `config.json`.
3. `{}`.
Which preset is consulted follows from the capability’s type. A plugin-backed option declares the capability type it accepts (`ConfigOptionDef::plugin_type`) and belongs to exactly one preset type, so `slicing-pipeline` capabilities are configured by the process preset and `printer-connection` capabilities by the printer preset. Nothing is hardcoded: declaring `plugin_type` on a new option is all it takes to place a new capability type on that map.
`get_config_version()` reports the version stamp from whichever level supplied the configuration, so a plugin migrating a stale config is never handed one level’s data with another level’s version.
`save_config()` from Python always writes the global configuration, never a preset — presets are the user’s to edit, and a plugin saving from a worker thread cannot mark one dirty. A capability whose active preset overrides it will therefore keep reading that override back rather than what it saved.
### Storage
A preset’s overrides live in an ordinary string setting on the preset (`plugin_preference_overrides`), holding a JSON array of entries keyed by plugin and capability. Because it is an ordinary setting, the whole preset lifecycle carries it for free: the dirty marker, the revert arrow, inheritance, project (3MF) round-tripping, and preset sync all behave exactly as they do for every other setting. The dialog is a pure editor over that text — it never writes to the preset itself and never writes to the global config file.
## Configuration Storage
All global plugin configuration is stored in a shared file:
`data_dir()/orca_plugins/config.json`
Configuration entries are isolated by plugin and capability. The host also records the plugin version that last wrote each entry.
The configuration file is intentionally stored outside individual plugin directories. This allows settings to survive:
- Plugin upgrades and reloads
- Local plugin deletion and reinstallation
- Cloud plugin unsubscribe and resubscribe operations
Reinstalling or resubscribing to the same plugin restores access to its previously saved configuration.
## Known limitations
**Filament capabilities cannot be overridden per preset.** There is no single active filament preset — one is selected per extruder — and `get_config()` does not say which extruder the capability is running for, so a filament override could only be applied by guessing. Rather than hand a plugin another extruder's settings, filament capabilities read the global configuration.
Nothing reaches this today: no filament option declares a `plugin_type`, so no capability type maps to the filament preset. Lifting it means pushing the extruder onto the plugin call context the Python trampoline already maintains and resolving the preset from that, with the extruder optional — whole slicing steps (`posSlice`, `psGCodePostProcess`) span every extruder and have no current filament.
# Tests
`tests/slic3rutils` covers the capability config store, the Python config API, the preset override layer, the capability-type → preset-type mapping, and which capabilities a preset counts as in use.
# Screenshots/Recordings/Graphs
Custom UI
<img width="855" height="703" alt="image" src="https://github.com/user-attachments/assets/745ecb7d-9e20-4c39-b857-5aa730a27142" />
Default JSON text editor
<img width="855" height="703" alt="image" src="https://github.com/user-attachments/assets/18b7b89c-6f77-4960-a9b2-964e71f74fc3" />
Process Sidebar
<img width="717" height="360" alt="image" src="https://github.com/user-attachments/assets/8fac3e66-c06a-44e4-ad4b-4cc6c003bb2b" />
Filament dialog
<img width="1090" height="832" alt="image" src="https://github.com/user-attachments/assets/ff6a4cbe-11c0-4d04-9ecb-9a717bdeb3f4" />
Printer settings dialog
<img width="1090" height="832" alt="image" src="https://github.com/user-attachments/assets/c616afb0-4eb2-40c2-92c0-7f5edc50b4e6" />
Dialog opened from preset settings
<img width="860" height="725" alt="image" src="https://github.com/user-attachments/assets/069408a8-e94b-47e0-8e16-a81e0d58b4d4" />
# Example plugin with custom UI used in screenshot
[custom_ui_screenshot_demo.py](https://github.com/user-attachments/files/29995212/custom_ui_screenshot_demo.py)
<!--
> 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)
The preset option was plugin_preference_overrides while the GUI field type
that renders it was GUIType::plugin_config, for one and the same thing.
Settle on "config": the store, the dialog and the Python hooks all say config
already, so renaming that way touches 4 files instead of the whole plugin
subsystem and the public plugin API.
Keep the _overrides suffix — the option is the preset's override layer over
the base PluginConfig store, a distinction EffectiveCapabilityConfig tracks.
Also wrap the printer tab's group heading in L(); it was the only one of the
three missing it, and was therefore untranslatable.
Replace hand-rolled nozzle type comparison + Hybrid hack with
BBS-style NozzleGroupInfo comparison in check_ams_status_impl.
Previous approach: direct nozzle_volume_type == printer_flow_type
with a Hybrid tolerance lambda. This either suppressed the dialog
entirely (Hybrid always matched) or showed it on every Preview switch.
New approach (matching BBS):
- Build preset NozzleGroupInfo from extruder_nozzle_stats config
- For Hybrid presets: expand into per-type counts (Std#N, HF#M)
- nozzle_count==0 (never synced): dialog appears for first sync
- nozzle_count>0 (after sync): compare with printer GetNozzleGroups()
- Counts match → dialog suppressed on Prepare↔Preview switches
Safe for all multi-extruder printers (H2C, H2D): non-Hybrid presets
use single NozzleGroupInfo per extruder. Single-extruder printers
exit at is_multi_extruders() guard before reaching this code.
Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp
is_extruder_stat_synced() line 16642
There is no arm64 self-hosted build server, so when \`vars.SELF_HOSTED\`
is set the arm64 Linux and Windows legs previously fell back to
GitHub-hosted runners. Drop those legs entirely instead, along with the
unit test jobs that consume their artifacts.
**Changes:**
- **Linux / Windows builds:** matrices switch from a static \`include:\`
list to \`fromJSON(vars.SELF_HOSTED && ... || ...)\`, so self-hosted
runs build x86_64/x64 only. The Windows job's per-arch runner
conditional is gone — the runner is now baked into each matrix entry.
- **Unit tests:** \`unit_tests_linux_aarch64\` and
\`unit_tests_windows_arm64\` are gated on \`!vars.SELF_HOSTED\`; their
\`needs\` still succeed, so \`success()\` alone would not skip them.
- **Slice check:** the profile validator artifact is now named per-arch
and uploaded from the aarch64 leg normally, or x86_64 on self-hosted.
The job's runner and download name follow the same switch, keeping the
gate alive rather than failing on a missing artifact.
- **Comments:** trimmed across the touched blocks.
No change when \`SELF_HOSTED\` is unset — an unset variable is falsy, so
GitHub-hosted runs keep both arches, the same runners, and the aarch64
slice check.
The merge kept this branch's PluginConfig design, which deletes
PluginDescriptor::settings, get_plugin_settings() and ctx.params, but left
references to them behind: the slic3rutils target did not build, and the
bindings test still asserted the removed ctx.params attribute.
Port the two settings tests onto PluginConfig instead of dropping them. They
guard a field bug where a cloud-metadata refresh wiped a plugin's settings and
it silently ran on its own defaults, so the equivalent properties are still
worth pinning: that a stored config survives the refresh, and that an edited
config reaches the plugin through a real dispatch.
Also defer PluginsConfigDialog's web commands off the webview script-message
callback, as PluginsDialog already does. Its remove_preset_override handler put
a modal wxMessageBox on that stack, which is the GTK crash class fixed in
b779a7bfed/f2ccbfc8b5 for the sibling dialog.
Enable use_forcast in reorder_filaments_for_minimum_flush_volume_base
to match the multi-extruder path behavior (line 1227).
The forecast solver (solve_extruder_order_with_forcast) considers the
next layer's filament set when choosing ordering for the current layer,
minimizing inter-layer transition flush cost.
Previously disabled (hardcoded false) in the single-nozzle/base path,
causing suboptimal inter-layer transitions. The multi-extruder path
already had this enabled.
Measured on 5cubes (5 filaments, 35 layers, H2C):
- Print time: -12 min (-10%)
- Waste filament: -5g (-28%)
- WT extrusion: -44%
Limited to ≤5 filaments per nozzle per layer (O(N!×M!) complexity).
Clamp ramming speed during extruder changes so the departing nozzle
has enough time to reach precool_target_temp before carousel rotation.
Only applies to extruder changes (not carousel nozzle changes).
Reference to BBS: BambuStudio/src/libslic3r/GCode/WipeTower.cpp
ramming() L3449-3462
Physical dist/speed ignores acceleration/deceleration, giving
underestimated total time (29 min vs real 48 min). M73 from the
trapezoid planner accounts for accel/decel and is closer to reality.
Now we keep physical DISTRIBUTION (proportions per-filament) but
scale the X-axis so total time matches M73 trapezoid estimate.
Replace M73-based timeline interpolation with physical time calculation
from G1 feedrates and M400 delays. M73 has 1-minute resolution and
non-uniform granularity which distorts the time axis (e.g. P83→P100
jump makes last filament appear much longer than it actually is).
Physical timeline computes cumulative time per gcode line from actual
move distances and feedrates, giving accurate filament duration on plot.
Falls back to M73 interpolation when raw gcode lines are not available.
End gcode contains firmware-conditional M400 waits for air purification,
timelapse capture, and sound notification that are post-print operations.
These were incorrectly included in M73 total time, inflating the estimate.
The fix detects MACHINE_END_GCODE_START tag during the streaming parse
(process_tags) and sets m_skip_end_gcode_delays=true. process_M400 then
skips timed delays (S/P params) in the end gcode scope.
BBS achieves the same effect by dropping leftover in calculate_time
(is_final=true). We skip at the source instead, which is more surgical
and leaves calculate_time behavior unchanged for all printers.
Affects all BBL printers with MACHINE_END_GCODE_START tag.
Non-BBL printers are unaffected (no tag = no skip).
Cloud catalog records never carry [tool.orcaslicer.plugin.settings], so the
metadata merge wiped the locally-parsed settings and plugins silently ran on
their built-in defaults (ctx.params arrived empty).
The M620→M621 firmware toolchange block weight (500x) was only applied
to sparse track sample lines. Hundreds of G1 moves between samples
inside the M620 block got weight=1, causing firmware toolchange to
appear compressed on the timeline plot.
Build continuous M620→M621 line ranges from track samples and apply
weight=500 to ALL lines within those ranges. This makes toolchange
and wipe tower zones proportionally accurate on the timeline.
Update Maschine G-Code according to latest Bambu Studio Version X2D
filament_change gcode: 2026/07/01
X2D layer_change gcode: 2026/07/01
X2D start gcode: 2026/06/05
X2D timelapse gcode: 2026/06/03
# 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?
-->
# 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.
-->
<!--
> 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)
Add two standalone Python tools for deep comparison and analysis of .3mf
slicing project files:
- compare_slices.py: comprehensive slice comparison with filament usage,
nozzle mapping, tool change sequences, prime tower analysis, temperature
timeline, and automatic critical discrepancy detection
- show_temp_plot.py: interactive HTML temperature timeline plotter for
visualizing heater profiles during multi-nozzle prints
Both tools use only Python stdlib (no external dependencies).
Primary use case: regression testing H2C carousel purge volumes and
BBS compatibility verification.
The upstream _make_wipe_tower() tracked purge volumes per-extruder (2 slots),
which collapsed all H2C carousel filaments into one slot and caused massive
redundant AMS flushing (~40g instead of ~0.4g).
Changes:
- Print.cpp: Replace per-extruder nozzle_cur_filament_ids with BBS
NozzleStatusRecorder that tracks per group_id (carousel slot 0..6).
Use get_nozzle_for_filament() to resolve physical slot per layer.
Select filament_prime_volume_nc for nozzle changes, filament_prime_volume
for filament changes (BBS pattern).
- PrintConfig.hpp/cpp: Register filament_prime_volume (per-filament EC prime
volume, default 45mm³) matching BBS PrintConfig.
- Preset.cpp: Add filament_prime_volume to preset keys list.
Safe for non-carousel printers: group_id == extruder_id when each extruder
has one nozzle, so NozzleStatusRecorder behaves identically to the original
per-extruder tracking.
Reference to BBS: BambuStudio/src/libslic3r/Print.cpp _make_wipe_tower() L3341-3392
open_terminal_dialog is reached from the plugins dialog's webview
command, and TerminalDialog hosts a webview of its own — same class as
the plugin-window crash. Defer the window work via CallAfter, guard the
re-front Show() per #13657, and drop the redundant Raise() on creation.
wx 3.3.2 delivers webview script messages synchronously inside the native
callback on GTK and macOS, so script plugins run with the plugins-dialog
webview's signal/delegate frame on the stack. Creating and presenting the
orca.host.ui window from there crashed on Linux at Raise() --
gtk_window_present while GTK's deferred show was still in flight.
Defer the whole window creation to a CallAfter with a pre-bound registry
handle (post/close stay FIFO-safe, teardown races become a no-op), and
drop Raise() plus the show_modeless_dialog wrapper: Show() already
activates and fronts a new window on every platform.
Drop the orphaned PluginCallbackList (dead
after the PluginManager migration) and hop
run_on_*_callbacks onto the UI thread via
CallAfter, snapshotting under the mutex on the
worker first. Keeps wx subscribers off the
detached load/unload workers.
Only one action source ever existed, so the
IActionSource interface and ScriptActionSource
are gone. ActionRegistry now subscribes to the
plugin loader and enumerates actions directly
in init() - no polymorphism for one impl.
Replace the opaque FNV-hash SpeedDialActionId
with a readable composed id of the form
prefix:title:source_key. Split AppAction's
single source field into source_key (stable
identity, e.g. plugin_key) and source_name
(display), so identity and display no longer
share one field.
A network-plugin hot reload unloaded the DLL without destroying the agent
handle it created, so the plugin's m_agent was left dangling into freed
memory. On reload create_agent() short-circuited on has_agent() and kept the
stale handle, and the next call into the new DLL (install_device_cert from the
device-refresh timer) dereferenced it and crashed with an access violation.
unload() now destroys the agent first, so a reload always gets a fresh handle.
When a print fails, DeviceErrorDialog now fetches the printer's captured camera
frame of the failure and shows it in place of the generic HMS illustration,
falling back to the local image and a drawn placeholder on older plugins or errors.
- Agent: add get_hms_snapshot through NetworkAgent / IPrinterAgent (BBL calls the
bound plugin symbol; other agents no-op, so old plugins degrade gracefully).
- DeviceManager: parse and clear m_print_error_img_id from the print-error message.
- Dialog: tiered cloud/local/placeholder image reusing the single image widget,
with a liveness-guarded async callback decoded on the UI thread.
Add the 11 functions the newer plugin exports (HMS snapshot, GoLive camera
URL, consent report, cloud filament-spool CRUD, AMS-filament sync, login-state
and studio-info hooks) plus their four parameter structs. Everything resolves
to null on older plugins and no caller invokes them yet, so this is inert ABI
surface for later features.
The newer plugin adds four PrintParams fields (task_timelapse_use_internal,
extruder_cali_manual_mode, svc_context, slicer_uid) and an extra dev_model
argument to bind. Both cross the by-value C ABI boundary, so match the struct
layout and thread dev_model through the bind chain, else start_print and bind
corrupt the stack on the newer plugin. Keep 02.03.00.62 selectable as a fallback.
## What this does
Ports the AMS filament drying control feature from BambuStudio. Most
work was done by Claude Code with deepseek-v4-pro. Thanks Bambu & CC &
DeepSeek :P
Allows users to start, monitor, and stop AMS-based filament drying
directly from the OrcaSlicer UI for N3F (AMS 2 Pro) and N3S (AMS HT) AMS
units.
Mostly from
https://github.com/bambulab/BambuStudio/commit/c8f70c6ca76e53775aa021a197fe9ec972db709e
Part of #12091
## Screenshots
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/24f579cb-c67c-4d6e-bf77-c31e018f2f70"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/74f628aa-6f5f-4150-b2e9-082e4ffc3527"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/d2f26412-a054-4085-9236-5074a030b001"
/>
<img width="500" alt="image"
src="https://github.com/user-attachments/assets/7beef770-8fbc-4375-b244-e0de44b8db2f"
/>
## Changes
- **Data model:** Added drying status enums (`DryStatus`,
`DrySubStatus`, `CannotDryReason`, etc.), `DrySettings` struct,
`DevFilamentDryingPreset` struct to `DevAms`/`DevFilaSystem`
- **Promoted `DevAmsType`** to a global enum (`EXT_SPOOL=0, AMS=1,
AMS_LITE=2, N3F=3, N3S=4`), renamed `DUMMY` → `EXT_SPOOL`
- **JSON parsing:** Extended `DevFilaSystemParser` to parse drying
status fields from printer status messages
- **Commands:** Added `CtrlAmsStartDryingHour()` and
`CtrlAmsStopDrying()` sending `"ams_filament_drying"` JSON via MQTT
- **Backend utility:** New `DevUtilBackend` class with
`GetFilamentDryingPreset()` for reading filament drying config keys
- **UI dialog:** New `AMSDryControl` dialog with three pages
(status/control, guide, progress) matching BambuStudio behavior
- **Integration:** Wired AMS humidity indicator click to open the drying
dialog for N3F/N3S AMS types
- **Assets:** 12 new drying-related images from BambuStudio
- **Firmware parsing:** Added `is_support_remote_dry` flag parsed from
`fun2` bit 5
## Constraints
- N3F/N3S only — standard AMS and AMS Lite continue to use the existing
humidity popup
- Backward compatible — existing `command_ams_drying_stop()` preserved
<!--
> 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)
The eight presets added in 0545750c1b never got ids: the six iQ processes
had none, and K1 SE 0.8 / V-Core 4 0.8 carried ids copied from the presets
they were duplicated from — K1 SE 0.8 still shared K1C 0.8's id. Regenerated
with scripts/assign_vendor_setting_ids.py; none of the eight have shipped in
a release, so no existing id changes meaning.
* fix: impl refactor
* fix: unload/load python module, race conditions, freezes
* remove dead code
* remove extra hook
* remove more dead code
* fix gil run script
reslice() now enforces only the missing-plugin block via
refresh_missing_plugin_block (no second Print::validate); plate
ready-status returns to upstream's plain model_fits, matching
GLCanvas3D::reload_scene. Also restores main's use_bbl_device_tab and
the check_track_enable comment, and drops two unused MainFrame includes.
Merge-resolution cleanup. The #12506 re-select path kept main's preset_bundle
null check, and both select_machine calls now use effective_agent_id rather than
mixing it with the equal-but-differently-named agent_info.id.
Resolve five conflicts, all of which needed both sides rather than a pick:
- BackgroundSlicingProcess: ours was a pure tabs->spaces reformat of base, so
keep main's per-filament volume/nozzle map read-back (its only change here).
- GUI_App: main's #12506 else-if attached to an `if` this branch deleted;
re-expressed onto the same-agent early-return path (the agent factory caches
per id, so pointer equality is the same predicate).
- MainFrame: both sides relocated Sync Presets independently; keep main's
push_notification plus the branch's Plugins menu items.
- Tab: the "TODO: Orca: Support hybrid" blocks were unchanged base, not a branch
decision; take main's enabled Hybrid to match the already auto-merged siblings.
- test_config: union of both sides' cases (6 plugin + 9 multi-nozzle).
The filament-group golden harness landed with H2C/A2L support (#14685). Its
"FilamentGroup golden regression" / stress_66 case fails intermittently on
Windows x64, on main and on unrelated PRs alike. The test depends on how fast the
runner is.
The k-medoids clustering these goldens exercise is an anytime search bounded by a
3 second wall clock. Every restart is seeded from its own index, so nothing about
it is random. What varies is how many restarts fit in the budget, and the best
cost is a minimum over completed restarts, so a slower runner is never better.
Grading a score produced that way measures the machine as much as the code.
Add a ClusteringBudget struct and let the tests set it. The defaults are the
current 3 seconds and 30 restarts, so slicing behavior is unchanged. A
non-positive timeout removes the wall clock and bounds the search by restart
count alone.
The goldens are then graded under a fixed budget of four restarts, where every
one of them reaches the BambuStudio reference within 3%, so the score becomes a
property of the code. This retires the machine-specific 125103 lock on stress_66.
The default wall-clock path keeps its own test, asserting the grouping is valid
and the search does not run away. It makes no score assertion, because under a
wall clock that number is not a property of the code.
The golden test also checks the run fits in ten times the default wall clock.
Slicing quality depends on how many restarts fit in the budget, so a search an
order of magnitude slower would degrade real groupings while a fixed-budget score
gate stayed green.
The 3% tolerance stays as the parity allowance against the goldens. It also
covers a small spread across standard libraries: the k-medoids search seeds each
restart with std::shuffle, whose algorithm the C++ standard leaves unspecified,
so libstdc++, libc++ and the MSVC STL permute the same seed differently, start
from different medoids, and settle on slightly different groupings, about 3e-4
apart and only on the goldens heavy enough to reach the k-medoids search.
# Description
Adds a --slice (-s) mode to the profile validator that slices a
two-colour cube through every shipped printer, expanding all custom
g-code (change_filament_gcode, machine start/end, etc.). This catches
invalid-placeholder / bad-flow / slicing errors that the static JSON
checks and unit tests can't see.
Included:
- Validator: new -s sweep mode; per-profile error attribution in the
log; resolves the synthetic 2nd-filament nozzle-mapping so multi-nozzle
BBL printers (incl. the Direct-Drive+Bowden X2D) validate cleanly.
- CI, two complementary paths:
- check_profiles.yml — runs the sweep on profile-only PRs (nightly
binary).
- build_all.yml — new parallel slice_check_linux job runs it on
engine/src PRs with the PR-built binary (build_all doesn't trigger on
resources/**, so no overlap). Runs off the build's artifact, so it
doesn't lengthen the build leg.
- Profile fixes surfaced by the sweep: Creality, FLSun, Ginger, Qidi,
RatRig, iQ.
- Engine: whitelist BBL firmware T-opcodes (T1001/T65279/T65535) in the
time estimator (log-only, no g-code change); dedupe a
per-filament/per-layer log flood in get_config_index.
# 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.
-->
<!--
> 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)
The Unit Tests job sparse-checks-out only .github/scripts/tests, so the
baked-in absolute PROFILES_DIR was missing at runtime; the shipped-profile
test then read a non-existent JSON and null-dereferenced in opt_string.
Check out resources/ in the unit-test job, and guard the test helper to
skip when the profile is absent and require the key before dereferencing.
query_topology: indexing a body face-by-face was quadratic — face_by_index
re-walks the explorer and edge_by_index rebuilds the whole indexed map on every
single call. On a 15.7k-face / 25.6k-edge imported solid this blew past the
MCP 15 s main-thread timeout with the UI frozen throughout. GeometryEngine
gains faces_of()/edges_of(), which enumerate once in the very same order (ids
stay interchangeable with the _by_index accessors, so fillet/up_to_face targets
are unaffected). Measured on that body: 15 s timeout -> 0.46 s.
Feature ops: every commit-time m_doc.recompute() (fillet, cut, shell, boolean,
extrude, ...) now goes through recompute_guarded(), which runs the rebuild on a
worker thread. Live-preview/drag paths stay inline on purpose — yielding inside
a drag would be worse than the stall.
run_off_ui_thread(): the progress dialog is now created only after 300 ms, so a
fast op does not flash a dialog, while input stays blocked (wxWindowDisabler)
for the whole operation since the worker owns the document.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Viewport: GLCanvas3D::on_idle() cleared m_dirty even when
_refresh_if_shown_on_screen() rendered nothing because the canvas was not on
screen yet — a frame requested while the notebook was still showing a page got
silently swallowed and the viewport stayed blank until some later event dirtied
it again. _refresh_if_shown_on_screen() now reports whether it rendered, and
on_idle keeps the canvas dirty when it did not. Covers Design/Prepare/Preview.
Freeze: a big STEP (17.8 MB) spent ~40-50 s inside OCCT on the UI thread
(read_step_solids + recompute), so the window stopped repainting and the
compositor marked the app unresponsive. Both now run on a worker thread behind
an app-modal pulsing progress dialog: the UI keeps painting and the document
cannot be touched while the worker owns it. OCCT's Standard_Failure is not a
std::exception, so the worker catches it explicitly — an escaping exception
would terminate the process.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
request_repaint() only invalidates the canvas (Refresh()) on the hardware-GL
path and relies on a wxEVT_PAINT to follow. When the notebook re-shows the
Design page, that invalidation is issued mid-show and dropped: no paint event
arrives, the canvas never renders, and the pane stays blank until another tab
switch forces an expose. Software GL renders directly, so it never showed there.
force_repaint() defers past the show, then Refresh() + Update() for a
synchronous paint. Called from on_tab_shown().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The new "Prime-tower visits..." test from #14685 (H2C/A2L support) throws
"Coordinate outside allowed range" at random on CI, on both Windows arm64 and
Linux x86_64. It's an uninitialized read of WipeTowerData::height.
#10780 (H2D/H2S) added a second wipe tower path, generate_new(), that fills in
depth, bbx, brim_width and rib_offset but not height. The older generate() sets
height, and clear() never did, so on the generate_new path it stays garbage.
first_layer_wipe_tower_corners() passes height to get_wipe_tower_cone_base() as
R = tan(cone_angle/2) * height. The stray bytes are usually zero, so R is zero
and the slice is fine, which is why it passes most runs on every platform. When
they aren't zero the cone radius runs past ClipperLib's limit and the slice
throws. Nothing selects for it, so it just flakes around.
#14685's test is the first to exercise this path, so that's when it started
showing up.
Initializing height in clear() fixes it, same as the m_origin fix in #13712. The
BBL generate_new path has no stabilization cone, so height = 0 is right.
Keep action identity and display metadata
constructor-set so registry keys cannot drift
from the objects they index.
Require sources to publish unique ownership while
the registry retains shared keepalive for runs.
Preserve opaque IDs and persisted state keys.
Lifecycle callbacks can be registered while
another thread dispatches them, risking races
and iterator invalidation.
Snapshot shared callback objects under a mutex,
then invoke them unlocked to preserve reentrancy
and mutable callback state.
CLI: guard 4 null derefs when loading a 3mf with no preset ids
At OrcaSlicer.cpp:1700-1704 the post-load block reads printer_settings_id,
print_settings_id, filament_settings_id, and nozzle_diameter from the
config that the 3mf carries. If the 3mf is a BBL/BBS-flavored 3mf but
was produced by a non-GUI writer (e.g. CLI --export-3mf without a
loaded preset) any of those keys can be absent, and config.option<T>(...)
returns nullptr — the ->value / ->values deref then SIGSEGVs.
Wrap each optional lookup in an if-let. printer_model, printer_extruder_variant,
and print_extruder_variant already pass create_if_missing=true and are safe.
Repro (BEFORE this patch):
orca-slicer --export-3mf out.3mf in.stl # produces preset-less 3mf
orca-slicer --info out.3mf # SIGSEGV at :1700
bt: __cxx11::basic_string::_M_assign
-> Slic3r::CLI::run @ OrcaSlicer.cpp:1700
AFTER: --info out.3mf returns exit 0 with the mesh summary.
The same failure mode affected --inspect-mesh, --inspect-paint, and
every other action that has to walk the loaded model's config; --slice
would only survive because it always injects a printer via
--load-settings.
For non-BBL host printers (Moonraker/Klipper, Qidi, Snapmaker, Creality), switch_printer_agent() only re-selected the machine when the agent type changed. Switching between two printer presets that use the same agent left the selected machine and the agent's cached device_info pointing at the previously active preset's host, so filament sync kept hitting the old printer.
Re-select the machine when the agent type is unchanged but the target host differs, so the selected machine and device_info always follow the active printer preset.
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
Plater's pImpl (unique_ptr<priv> p) is destroyed before the wxWindow base
destructor runs DestroyChildren(), so child GLCanvas3D windows are torn down
after p is gone. GLCanvas3D::~GLCanvas3D() -> reset_volumes() then dereferences
the freed p through two paths:
- Selection::clear() -> plater()->canvas3D() -> p->get_current_canvas3D()
- _set_warning_notification() -> plater()->get_notification_manager()
Guard both with the existing wxGetApp().is_closing() flag; both are UI-only
side effects that are no-ops during shutdown, so normal-use behavior is
unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix text rendering issue when using MacType, with the original harmony os sans installed globally (OrcaSlicer/OrcaSlicer#14566)
The new fonts are generated using fonttools that only add gasp table to the original font without touching other parts
Move plugin capability enumeration, loader
subscriptions, and action construction into
ScriptActionSource. Keep ActionRegistry focused
on generic action state, dispatch, and snapshots.
Use source rather than package for generic origin
metadata so future non-plugin providers share the
same interface. Action IDs and persisted
configuration remain unchanged.
Subscribe before initial enumeration so plugin
events cannot be missed between the snapshot and
callback registration. Add a focused test for
source startup.
Decouple registry dispatch from plugin-specific
result types so future action types can provide
their own run implementations.
Store actions behind shared pointers to keep them
alive while plugin UI pumps a nested event loop
and queued refreshes mutate the registry. Scope
"don't ask again" to an individual action instead
of its whole plugin.
* Fix issue that switching printer profile is slow.
`wxGetApp().get_tab(preset_type)->select_preset(preset_name);` is called twice when switching printer profiles. Only one needed.
* Avoid unnecessary type conversion & function call during printer profile switching:
- Don't call `config->opt_string("printer_model")` repeatedly
- Use ref when possible during iterating
- Avoid unnecessary `wxString` to `std::string` conversion
Follow-up to the rack-aware pre-print checks: after the blocking checks
pass, warn (without disabling Send) when the plate needs more matching
hotends than the rack printer currently holds - suggesting rack setup,
a nozzle info refresh, or a re-slice to avoid filament waste - or when
the only matches rely on unreliable nozzle information.
Text-only warning rows; this message board has no refresh or
don't-show-again buttons.
A print sliced for a nozzle that sits in the hotend rack (but is not
mounted) was blocked by the send dialog's mounted-nozzle diameter check,
even though the printer fetches the required nozzle itself (#14685).
Consolidate the three mounted-nozzle gates (_is_nozzle_data_valid,
is_nozzle_type_match, _is_same_nozzle_diameters) into a single
CheckErrorExtruderNozzleWithSlicing fed by s_get_slicing_extuder_nozzles,
which collects the plate's per-extruder nozzle requirements (hybrid
extruders contribute one entry per used sub-nozzle flow). The rack
extruder validates against its whole inventory (mounted + rack) with
guidance to calibrate the rack, refresh nozzle info, or re-slice, and
blocks when toolhead + rack are full (no free slot to stow a nozzle).
Other extruders keep the validity/flow/diameter checks against the
mounted nozzle.
Also add CheckErrorRackStatus, which holds Send while the printer is
still reading the rack hotend information, and judge material hardness
for the rack extruder per dispatch-mapped nozzle as a non-blocking
caution (mounted nozzles keep the blocking gate).
A dual-nozzle H2C print with support filament hangs at its first nozzle
switch. The emitted file shows the change-filament block's M620 O ordinal
jumping from O1 straight to O230, plus a duplicate "M1020 S<n>" toolchange
command right after every change block. Two causes, fixed together because
they interlock (the ordinal check keys off the same toolchange detection
that suppresses the duplicate):
- append_tcr incremented m_toolchange_count once per prime-tower visit
(roughly once per layer), while the change-filament template only emits
its M620 O{toolchange_count + 1} line on real filament changes. With 229
change-less sparse tower layers below the first support layer, the first
real change reported ordinal 230. The counter now advances only when the
expanded change block really contains a toolchange command, and the
placeholder exposes the upcoming change's ordinal (count + 1). The
set_extruder path already counted per real change and is unchanged.
- toolchange_prefix() returned "M1020 S" for BBL printers, so the
custom_gcode_changes_tool() dedup could never match the stock profiles'
line-leading "T[next_filament_id] ..." commands and the writer's own
toolchange was appended after every change block on dual-extruder
machines. The prefix is now the plain "T" (the manual-filament-change tag
branch stays first), and the M1020 form moved into GCodeWriter::toolchange()
as an explicit branch that also carries the nozzle:
"M1020 S<filament> H<nozzle>". The nozzle parameter is signed on purpose:
the null-safe nozzle lookup legitimately yields -1, matching the stock
templates' own H-1 convention.
The prefix change also lets the CoolingBuffer recognize the change blocks'
T commands as tool boundaries on BBL printers (its per-filament attribution
previously keyed off the duplicate M1020, or nothing at all on
single-extruder models); its existing out-of-range guard ignores
T1000-class machine commands.
Verification: full suites green (libslic3r 48998 assertions / 169 cases;
fff_print 692 / 65 including three new scenarios - writer emission per
printer kind, dedup + ordinal progression on sequential prints, and a
prime-tower regression scenario verified to fail against the old per-visit
counting). Byte gate: 18 of 20 fixtures bit-identical; the sequential repro
differs by exactly its 3 removed duplicate M1020 lines, deterministic
across two runs. Reslicing the field project that exposed the hang yields
M620 O1 followed by a gapless O2..O59 and zero duplicate M1020 lines.
Co-authored-by: songwei.li <songwei.li@bambulab.com>
The time estimator's speed/acceleration limits were indexed by time
mode only, reading slot 0 of the per-(extruder x volume-type) arrays
the multi-extruder profiles already carry (H2C 0.4: 8 entries, H2D
0.4: 10). Every move was therefore modelled with the first machine
slot's limits regardless of which nozzle variant was printing -
estimation fidelity only, since emitted feedrates/accelerations are
decided on the slicing side.
Now the estimator resolves the machine slot of the nozzle currently
mounted in the active extruder: the nozzle grouping context is handed
to the processor BEFORE the streaming replay (new member + setter -
deliberately separate from the post-stream result-field handover that
gates the richer change-time model, whose timing is unchanged), the
occupancy recorder is populated on every filament change (bookkeeping
decoupled from the gated time model; recorder writes have no time
effect), and get_machine_config_idx maps (volume type x extruder type
x extruder) to the slot via the printer's variant layout, newly
carried on the processor result. The feedrate/acceleration getters
gain a slot parameter indexing [slot*2 + mode]; jerk and the
print/travel/retract accelerations stay mode-only. Reloaded sliced
projects re-estimate with the result's saved grouping context;
imported bare g-code degrades to slot 0 - the historical read.
M201/M203 write the parsed value into EVERY slot's mode entry (a
firmware envelope change is global), which keeps per-slot reads in
lockstep with the mode-only reads they replace: the fleet emits
envelope lines before any motion, so estimates - hence the estimated
time header, M73 lines, and every other byte - are unchanged (20/20
pinned-slice byte gate bit-identical, incl. the sequential repro
sliced twice). Fidelity improves where envelope emission is off or a
migrating per-layer plan moves filaments across variants.
Tests: a stub-driven processor case proving the slot follows the
active nozzle through the exact production path (T..H.. commands,
fallback recorder bookkeeping, 4x time ratio on the slow variant),
that emitted M201/M203 reach every slot, and that a missing context
degrades to slot 0. Suites green (libslic3r 48998/169, fff_print
667/62).
When a per-layer nozzle grouping migrates a filament across nozzle
variants, the write-back turns two groups of config arrays from
filament-indexed into column-indexed: the per-variant filament options
(one column per variant a filament uses) and the merged extruder
retract overrides (resized to the column count by apply_override).
Export-path readers that still indexed them with the raw filament id
read a neighbor's column for every filament ordered after a migrating
one: toolchange/standby temperatures (M104/M109), retraction lengths
and feedrates, wipe distance, z-hop types, air-filtration keys, and -
through the Extruder's cached flow term - the extrusion E of every
move.
Now every such read resolves its column through the existing
layer-aware resolver (get_filament_config_index ->
Print::get_filament_config_indx), which returns the raw filament id
whenever no per-layer grouping result is published, so static prints
are byte-inert by construction. The Extruder itself has no layer
knowledge, so it gains an injected config column (set_config_index,
default = filament id) that the generator refreshes at the only two
resolution-changing events - layer change and writer toolchange - and
that re-syncs the cached e_per_mm3 flow term. Old-filament reads
resolve at the current layer, which is safe because the per-layer maps
are gap-filled carry-forward. Whole-array placeholder copies
(toolchange temperature overrides) are rebuilt in filament order,
mirroring the existing per-variant placeholder remap. The resolvers
move to the public section so non-friend helpers (ooze prevention) can
resolve too.
Documented, deliberately unchanged: the wipe tower's per-filament
parameter rows (no layer dimension; tower x per-layer grouping is a
follow-up), travel_slope's physical-extruder read, estimator pre-heat
bookkeeping temps, and index-0 header diagnostics.
Verification: new Extruder column-injection scenario (defaults, column
follow + flow-cache rescale, filament-indexed reads unaffected, reset
semantics) and a migrating write-back case proving the column shift for
filaments ordered after a migrator and the resolver tracking it (11 +
14 assertions); suites green (libslic3r 48998/169, fff_print 655/61);
20/20 pinned-slice byte gate bit-identical (incl. sequential repro x2
deterministic).
When the per-layer filament selector (enable_filament_dynamic_map)
migrates a filament across nozzle variants (e.g. Standard -> High Flow),
the config write-back only stored the derived extruder map; every
per-variant filament value (retraction, nozzle temperature, flow,
flush...) kept the numbers resolved from the pre-slice static mapping.
Now both dynamic write-back sites (the by-layer branch and the
sequential stitch) branch on the result's dynamic support. Migrating
results run a mixed-filament expansion that regathers every
filament_options_with_variant key from the pristine per-variant
superset, giving a migrating filament one config slot per (extruder
type x nozzle volume type) it lands on - filament_self_index,
filament_extruder_variant, and all value arrays grow in lockstep - and
recompute the retract overrides with per-slot machine indices so a nil
slot falls back to its own variant's machine value. Non-migrating
dynamic results take the merged three-map write-back so re-applies
reproduce from the written maps. Unrouted filaments resolve from the
result's own default map, so slot resolution never depends on
filament_map round-tripping through the plate config.
Print::apply reproduces the identical expansion from the persisted
group result (shared dedupe helper, expansion function, and slot
indices on both sides): the expanded keys sit in the psWipeTower /
psGCodeExport invalidate lists, so without the reproduction every
re-apply after a selector slice would diff non-empty and permanently
invalidate. cal_non_support_filaments now resolves the extruder per
layer from the published result for dynamic groupings.
filament_map_2 keeps its apply-time static derivation; nothing on the
dynamic path reads it (the per-slot machine indices key the override
merge), and per-(extruder x volume-type) machine limits in the g-code
processor remain a documented follow-up.
Every change is gated behind is_dynamic_group_reorder() or a persisted
result with dynamic support; no profile sets the flag, so the static
fleet's instruction stream is unchanged (20/20 pinned-slice byte gate
identical, incl. the sequential repro sliced twice, deterministic).
Tests: expansion unit coverage (migrating slots, unrouted fallback via
the default map, mis-sized volume map ignored, nullable retract keys in
lockstep, slot machine index layout), an end-to-end stub-driven
write-back asserting expanded slots, per-layer config-index resolution,
the override merge incl. the nil-slot variant fallback, and re-apply
stability, plus a real selector slice staying valid across re-apply.
Suites green (libslic3r 48987/168, fff_print 633/60).
Sequential (by-object) prints were incoherent with the per-layer filament
selector (enable_filament_dynamic_map): the by-object branch published a
static grouping while each per-object ToolOrdering independently ran the
dynamic planner from an empty nozzle status and wrote its own map to the
config (one write per object, last object wins). The exported toolchange
sequences then disagreed with the published result that drives the
per-layer maps, placeholders, and selector emission.
Now the by-object branch, when the selector is enabled, plans each unique
object once — threading the physical nozzle occupancy and the previous
object's last filament into the next plan — stitches the per-object
per-layer nozzle maps into one print-wide result (gap-filled by the new
normalize_nozzle_map_per_layer so any layer index resolves a filament's
nozzle consistently), publishes it, and writes the derived extruder map
back once. The plans are cached on the Print and g-code export consumes
the cache: the ToolOrdering seed changes the plan input (dontcare
assignment, first-layer reorder), so a fresh export-time construction
could re-plan differently from the published stitch. The per-object
dynamic write-back is gated off for sequential prints.
Every change is gated behind is_dynamic_group_reorder(); no profile sets
the flag, so the static fleet's instruction stream is unchanged (20/20
pinned-slice byte gate identical, incl. the by-object repro sliced twice).
Tests: normalize unit coverage (carry-forward, back-fill, ragged input),
stitched-blocks selector detection, and an end-to-end by-object selector
slice (apply -> process -> export) asserting the published stitched
result, one cached plan per object, the config write-back, and a clean
export. Suites green (libslic3r 48958/165, fff_print 633/60).
# Slicing-pipeline plugins: Python hooks inside `Print::process()` with
an editable geometry API
This branch adds a new **slicing-pipeline** plugin capability on top of
the plugin framework: Python plugins can now run at defined points
inside the slicing pipeline and edit the live slicing data, with changes
cascading into perimeters, infill, and the final G-code.
## The capability
- New plugin type `slicing-pipeline`, selectable per print profile via a
new picker option (`slicing_pipeline_plugin`).
- `Print::process()` fires a hook at 13 pipeline steps (`posSlice`,
`posPerimeters`, … `psSkirtBrim`, `psGCodePostProcess`). Selected
plugins run per step with cancellation honored and failures surfaced as
ordinary slicing errors, never crashes.
- G-code post-processing is folded into this capability as the final
step (`psGCodePostProcess`); the separate "post-processing" plugin type
and its option are removed. Breaking only for the unreleased plugin API
— migrated plugins gain settings and config access in return.
## The Python API (`orca.host`)
- The live print graph is exposed as raw host classes — `Print`,
`PrintObject`, `Layer`, `LayerRegion`, `SurfaceCollection`, `Surface`,
`ExPolygon`, `Polygon`, `Point`, plus `Model`/`TriangleMesh` with
zero-copy numpy views.
- Geometry is genuinely editable through the class API: in-place
transforms, whole-surface replacement, and vertex-level rebuilds, with
`layer.make_slices()` re-deriving the C++ invariants after edits. Write
paths validate input, so malformed data raises in Python instead of
corrupting the slice.
- Bindings are organized under `src/slic3r/plugin/host/` by domain.
## Architecture
- All libslic3r hook seams (capability resolver, pipeline dispatcher)
are installed and uninstalled by one composition root,
`plugin/PluginHooks`, from `PluginManager::initialize()`/`shutdown()`.
GUI_App no longer accumulates per-capability wiring, and hooks detach
before the Python interpreter finalizes.
## Samples (`sandboxes/`) — one per editing idiom
| Sample | Step | Demonstrates |
| --- | --- | --- |
| **Inset Every Slice** | `posSlice` | Shrink every slice via polygon
offset + whole-surface replacement (`slices.set`) |
| **Twistify** | `posSlice` | Twist/taper/wobble via count-preserving
in-place transforms |
| **Fuzzy Slices** | `posSlice` | Fuzzy skin applied to the slice
contours themselves (vertex-level rebuild) — walls, infill, and the
preview all inherit it |
| **G-code Stamp** | `psGCodePostProcess` | Editing the exported G-code
file in place |
# Screenshots/Recordings/Graphs
1. Fuzzy skin example:
Orca's built-in fuzzy skin perturbs the outer-wall EXTRUSION PATHS
during
perimeter generation, so only the printed wall is fuzzy. This sample
instead
perturbs the sliced outline itself at sliced geometry:
<img width="1230" height="902" alt="image"
src="https://github.com/user-attachments/assets/bcf8b0c7-f932-4e6a-985d-7c9cc2f3d7cb"
/>
2. Twistify -- twist/taper/wobble any model at slice time
every layer's sliced surfaces are transformed by a similarity
about the object's bounding-box center as a function of Z
https://github.com/user-attachments/assets/d1309ea8-b01c-4708-adf1-821b3b00a4cc
3. Inset Every Slice -- a small, WORKING SlicingPipeline sample plugin
For every layer/region of the sliced object, this shrinks each
sliced surface by INSET_MM using a real polygon offset
<img width="1225" height="896" alt="image"
src="https://github.com/user-attachments/assets/5f2028a9-ae2a-4aea-8b38-a3d6e24f5cb4"
/>
Experimental fuzzy on geometry
Mirrors libslic3r's fuzzy_polyline on the slice contours at Step.posSlice,
demonstrating the count-changing mutation idiom (rebuild ring via
Polygon.append, write back via ex.contour / ex.set_holes). C++ analogue
test proves area preservation, cascade, and bounded displacement.
The Print-level LayeredNozzleGroupResult had a single producer, the
by-layer branch of ToolOrdering, which is gated to non-sequential prints.
The by-object branch in Print::process computed a grouping only in auto
map modes and never stored it, so a sequential slice exported with a null
group result: the per-nozzle placeholder tables came up empty and any
start g-code indexing nozzle_diameter_at_nozzle_id[] aborted with
"Indexing an empty vector variable". A prior by-layer slice masked the
bug by leaving its (never cleared) result on the Print.
Now the by-object branch runs get_recommended_filament_maps in every
static map mode (in manual modes the result mirrors the user's
assignment, deviations throw as in by-layer) and publishes it
print-wide. The config write-back stays gated to auto modes: in manual
modes it would only re-store the pre-slice values.
Regression test: a two-object by-object print must publish a non-null
group result and resolve nozzle_diameter_at_nozzle_id[] in start g-code
(both fail without the fix). Suites green (libslic3r 48929/162,
fff_print 633/60); 18-fixture byte gate identical; the by-object repro
project goes from the export error to valid g-code, determinism x2.
GUI_App::on_init_inner() carried the plugin dispatch policy inline (the
capability resolver and the slicing-pipeline dispatcher) and would grow
with every capability that fires from inside libslic3r.
plugin/PluginHooks.{hpp,cpp} now owns one file-local installer per hook,
aggregated by plugin_hooks::install() -- called from
PluginManager::initialize(), reset in shutdown() so no hook can enter
Python after the interpreter finalizes. The wx-side loader subscriptions
move into GUI_App::init_plugin_gui_wiring().
No behavior change; dispatch bodies moved verbatim.
* ENH: config: add logic to apply params to object/region config with multi-extruder
JIRA: no-jira
Change-Id: Ieab98cd8d031e5ca82a3aad2d0b89d8ae4a794f1
(cherry picked from commit 3179fd416e68ca8bc2d746f859508d07db18fe5b)
* FIX: X1C switch to H2D lose Highflow parameter
Jira: STUDIO-15272
Change-Id: Id8cf5d93a49d5542ac82f9554974b458e15c1193
(cherry picked from commit 15d9f072ff658a3beb4f916d978dfea12c2d9f16)
* Fix mishandling of `stride` param and add unit test for it
* Fix modified multi-variant per-obj option highlight
* Fix issue that per-obj FloatsOrPercents options are marked as dirty incorrectly when lost focus
---------
Co-authored-by: lane.wei <lane.wei@bambulab.com>
Co-authored-by: weiting.ji <weiting.ji@bambulab.com>
HoverLabel's constructor used SetSizerAndFit, which records the
count-hidden width as the panel's explicit minimum size. An explicit
minimum outranks best size in sizer allocation, so once the "(N)" count
was shown, any ancestor Layout() - e.g. switching the extruder flow type
to Standard or High Flow - shrank the title row back to the stale width
and clipped the trailing edit button. Hybrid only appeared correct while
nothing had re-laid the row since its own Fit().
Use plain SetSizer and, on every title/count change, invalidate the
cached best size and re-lay both the row and its parent so the sizer
always allocates the current content width.
Print::apply rebuilds m_config.filament_map_2 to the real per-filament slot
map on every apply, while the incoming full config only ever carries the
ConfigDef default. The resulting phantom one-key print_diff hit the
invalidator's catch-all branch and killed every print-level step on each
apply, so on multi-extruder printers a fresh slice result was invalidated
the moment the GUI re-applied after slicing completed.
Dropping the key from print_diff loses no information: it is never a user
input, and the rebuild derives it from filament_map, filament_volume_map
and the variant slots, each of which is diffed and invalidation-listed on
its own.
Regression test: re-applying an unchanged config after process() must not
invalidate psSlicingFinished (fails with APPLY_STATUS_INVALIDATED without
the fix). Suites green (libslic3r 48891/154, fff_print 631/59); 19-fixture
byte gate identical incl. the Hybrid repro project, determinism x2.
Bind libslic3r's TriangleMesh directly with a shared_ptr holder rather than
wrapping it in a HostTriangleMesh snapshot struct. ModelVolume.mesh() hands
out the volume's own shared_ptr (via const_pointer_cast, which only serves
the holder type), so the Python object pins the snapshot exactly as the
wrapper did, and the zero-copy views now use the Python object as their
array base — deleting the capsule machinery.
The wrapper's type-level constness becomes a documented rule instead:
handed-out meshes are copy-on-write snapshots shared across threads, so the
binding exposes only const methods; a future mutable-mesh API must operate
on plugin-owned copies handed back via ModelVolume::set_mesh.
No Python-visible change (orca.host.TriangleMesh, same methods/docstrings),
and plugins now hold the real class a future set_mesh() will accept.
Verified with slic3rutils and fff_print suites.
Deleting an object's instance leaves a stale (obj_id, instance_id) pair in
PartPlate::obj_to_instance_set until it is pruned. object_list_changed() runs
during the delete path and calls has_printable_instances(), which guarded only
obj_id and then indexed object->instances[instance_id] on the now-shorter
vector, dereferencing a garbage ModelInstance* and crashing with SIGSEGV. The
reported fault address (0x12a) matches a member read on that bad pointer.
Reuse the existing valid_instance() helper, which bounds-checks both obj_id and
instance_id, at every obj_to_instance_set scan that was missing the instance-id
check: has_printable_instances(), printable_instance_size(),
is_all_instances_unprintable(), get_extruders_under_cli(),
duplicate_all_instance() and set_pos_and_size() (the last had no bounds check at
all). valid_instance() is made const so the const CLI scan can call it.
Fixes#14159
- FilamentMapDialog's manual page understands volume types: a mixed
(Hybrid) extruder shows separate Standard / High Flow drop zones
with live sub-nozzle counts, a validation timer with an inline
error + "set nozzle count" suggestion, and composes a per-filament
volume map on OK (persisted to the plate or globally)
- switching an extruder's Flow type rewrites the affected plate map
entries; plate maps stay sized across filament add/delete/count
changes (values keep their filament, no index shift)
- CLI: manual mapping on multi-nozzle printers synthesizes the volume
map from extruder flow types when absent; nozzle-manual mode
requires explicit maps; computed maps land on the plate so exported
projects carry filament_volume_maps
- filament_nozzle_map joins the project options (selection seeding +
filament-count resizing)
- pot entries for the new dialog strings
All 19 reference fixtures byte-identical; slicing an exported Hybrid
project reproduces its g-code byte-for-byte with the volume map
round-tripped through model_settings.config.
PluginHostApi.cpp had grown into one TU holding the module entry point plus
three unrelated domains (presets, model/mesh graph, app access), and
PluginHostSlicing.cpp mixed ownable geometry value types with the
non-owning live print graph. Reorganize the orca.host surface into
plugin/host/ with one registrar per domain:
- PluginHost.hpp/.cpp entry point (replaces PluginHostApi)
- PluginHostBindings.hpp internal per-domain registrar declarations
- PluginHostGeometry.cpp BoundingBox, Point, Polygon, ExPolygon + ndarray parsing
- PluginHostMesh.hpp/.cpp TriangleMesh snapshot (own TU ahead of planned
mesh construct/mutate APIs)
- PluginHostPresets.cpp Preset, PresetCollection, PresetBundle
- PluginHostModel.cpp scene graph: Model, ModelObject, ModelInstance, ModelVolume
- PluginHostApp.cpp Plater + plater()/model()/preset_bundle() accessors
- PluginHostSlicing.cpp live print graph only, now with a single lifetime story
- PluginHostUi.hpp/.cpp moved unchanged
PluginBindingUtils.hpp stays at plugin/ root: it is shared with pluginTypes/
and tests, not host/-specific.
No Python-visible change: same submodules, class names and docstrings.
Verified with slic3rutils and fff_print suites.
- the g-code writer tracks the current layer id and resolves
FILAMENT_CONFIG/NOZZLE_CONFIG (plus every non-macro variant lookup,
toolchange placeholder scalars, and the change-filament flush
overrides) through Print's per-filament, per-layer config-index
resolvers instead of the filament->extruder collapse
- update_layer_related_config refreshes the per-layer
extruder/volume/nozzle maps in the writer config;
update_placeholder_parser_with_variant_params remaps the
filament-variant arrays into filament-id space for custom g-code
(Orca's flush placeholder computation moves inside it)
- the engine's concrete per-filament volume assignment now merges into
the config write-back (the temporary hold from the producer commit
is lifted together with these consumers), and the background process
reads the computed volume map back to the plate
- append_full_config dumps the resolved filament_map_2 slots
- update_used_filament_values gains a bounds guard
- tests: per-filament Hybrid slot resolution + null-result fallback
Result: on a Hybrid extruder, each filament's features slice with its
assigned sub-nozzle's variant values (speeds, volumetric limits,
retraction). Verified on a 4-filament H2C Hybrid project: outer walls
split into three feedrate populations (30/50/200 mm/s), toolpath
geometry byte-identical, deterministic across repeated slices. All 18
non-Hybrid reference fixtures stay byte-identical except the
filament_map_2 header value now showing the real slot. Auto grouping
ties (multiple zero-flush perfect matchings) may pick a different
filament-to-nozzle isolation than other slicers; verified co-optimal.
Opening an STL/OBJ in the Design pane now rebuilds it into a real OCCT B-rep
solid that the face/edge feature tools can operate on, instead of a print mesh.
GeometryEngine::mesh_to_brep is a native C++ port of mesh2step
(github.com/tommasobbianchi/mesh2step): vertices and edges are shared across
triangles at construction time (vertex cache by deduped index, edge cache by
unordered index pair), so no BRepBuilderAPI_Sewing pass is needed to rebuild the
topology afterwards, and watertightness falls out of the edge-usage counts for
free. An open mesh is returned as a shell and reported as such — never dressed up
as a fake solid.
It runs in-process on the OCCT kernel libslic3r already links, so no STEP file is
written or re-read. That is not an optimisation but the whole point: a faceted
STEP of a 62k-triangle mesh is ~149 MB and OCCT's STEPControl_Reader takes >300 s
to parse it back, so routing this through a file would hang the GUI.
Coplanar neighbours are merged (ShapeUpgrade_UnifySameDomain, 5° default) so the
body arrives with pickable CAD faces rather than one face per triangle — on the
20,656-triangle test part that is 20,614 faces down to 4,784. Without it the
import is technically a solid but nothing you can meaningfully fillet or extrude.
- Design pane: "Import mesh" button + Shift+M; warns above 50k triangles.
- MCP: import_mesh {path, tolerance, merge_angle_deg}, returning the full
conversion stats so a caller can tell an honest solid from an open shell.
- Catch2: cube round-trip (exact volume, 12 faceted faces, 6 after merge), open
mesh stays a shell, and the scale-independent sliver rule that a naive
area < tolerance^2 test would get wrong.
Verified end-to-end on the real 20,656-triangle ir3v2 hotend STL: reproduces
mesh2step's Python run exactly (20,614 kept, 42 degenerate, 0 boundary edges,
2 non-manifold edges, not watertight) and the resulting body's bbox matches the
one FreeCAD reports for the same part.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
- Print::update_filament_maps_to_config takes filament/volume/nozzle
maps, backfills an empty volume map from extruder types, rebuilds
filament_map_2, re-expands the per-filament variant arrays, and
recomputes retract overrides keyed by resolved slots
- grouping writes its result back in every non-sequential mode;
manual multi-nozzle grouping validates the user mapping and raises a
translatable error on deviation; the engine's concrete volume
assignment is deliberately not merged yet (per-filament arrays are
already consumed by filament id, so materializing High Flow now
would change motion before the layer-aware resolvers land)
- Print::apply treats the three map keys as engine outputs in auto
modes (erased from the diff and adopted), compares them against used
filaments in manual mode, and keeps the pre-expansion snapshot in
sync with the late normalization pass so rebuilt headers reflect the
sliced state instead of resurrecting stale values
- volume/nozzle maps and extruder_nozzle_stats join the invalidation
group of filament_map (wipe tower + skirt/brim)
- PresetBundle composes full configs with an optional per-filament
volume map (plate map, else defaults derived from each extruder's
flow type); project config keeps the map sized across filament
count changes
- PartPlate stores per-plate volume/nozzle maps; Plater injects them
at every slice-composition site (incl. g-code reload and wipe-tower
estimation); BackgroundSlicingProcess reads engine results back to
the plate in auto modes
- per-filament map trust guards relaxed to size-match everywhere now
that every producer sizes the map; single-filament explicit flow
assignments are honored
- tests: grouping volume maps stay concrete, merge semantics of
update_used_filament_values, single-filament override honoring
Motion g-code is byte-identical fleet-wide including Hybrid projects
(19-fixture gate + repro determinism double-slice). Header deltas:
the map keys now dump real values, and stale pre-normalization values
(e.g. enable_prime_tower on single-used-filament prints) no longer
leak into the config block.
Review the slicing-pipeline plugin comments for context a reader of the source
alone cannot follow, and rewrite them to stand on their own:
- drop pointers to uncommitted design/plan material ("§3.6 (Twistify design)",
"the brief's note", "Fix 4(a)/4(b)")
- fix dangling references to code this branch removed: the retired set_slices()
and view mutators, the former G-code post-processing capability/trampoline,
the "Post-processing" capability family, the pre-refactor array helper
- drop "v1"/"in v1" phase labels, keeping the behavior they described
- correct stale cross-references: Twistify.py -> the real sample path;
test_plugin_host_api.cpp:32-40 -> import_orca_module in python_test_support.hpp;
"the binding"/"graphs above" -> the named source
Comment/string-only; no code behavior change.
- Print::get_nozzle_config_index / get_filament_config_indx resolve a
filament's variant slot per layer from the nozzle group result, with
hashed index caches; when no group result is published (sequential
prints), they fall back to the static filament->extruder mapping so
behavior is unchanged
- filament_map_2 caches each filament's resolved print-variant slot;
rebuilt in Print::apply after the filament_map diff handling and in
the filament-map write-back
- filament retract overrides now key by slot indices: apply_override
fallback indexing flips to 0-based, Print::apply passes
filament_map/extruder indices, the write-back passes filament_map_2
(identical resolution while slots equal extruders)
- filament_volume_map/filament_nozzle_map/filament_map_2/
filament_self_index become PrintConfig static members (required for
member access); grouping input guards tightened so their registered
1-element defaults are never mistaken for real per-filament maps
(single-filament manual mode keeps the mix-marker fallback)
- update_filament_self_index_cache refreshed at every full-config
assignment
- tests: 0-based apply_override fallback, get_config_index_base
hit/miss/mixed-type cases
The resolvers are not consumed by the g-code writer yet. Non-Hybrid
g-code is unchanged except the config header, which now serializes the
three new static keys (defaults until the per-filament producer lands);
verified by the 19-fixture byte gate: 3 added header lines per fixture,
zero motion changes.
- get_extruder_nozzle_volume_count derives per-extruder volume-type slot
lists from extruder_nozzle_stats (absent stats = one slot per extruder)
- update_values_to_printer_extruders learns the slot layout: when any
extruder mixes volume types, option arrays keep one slot per
(extruder x volume type), extruder-ascending then volume-ascending;
single-slot resolution takes the filament's volume type on mixed
extruders
- update_values_to_printer_extruders_for_multiple_filaments applies a
per-filament nozzle_volume_type override from filament_volume_map
(when sized to the filament count) and remaps filament_self_index
through the same pipeline as every other filament key
- get_config_index_base + is_auto_filament_map_mode helpers (consumers
land with the per-filament config-index resolvers)
- callers updated: PresetBundle composition paths, PrintApply (counts
hoisted above the extruder_applied guard), Print write-back
- new tests: slot counting, Hybrid slot expansion incl. stride 2,
per-filament override, non-Hybrid degeneracy
Non-Hybrid printers keep their variant layout and values (proven by a
19-fixture byte gate; the only header delta is filament_self_index now
flowing through the same variant pipeline as its sibling filament
keys). Hybrid slices grow the config-block variant arrays to one entry
per sub-nozzle volume type; motion g-code is unchanged until the
g-code writer consumes the new slots.
Gate the entire parametric Design/CAD subsystem behind a single CMake
option so the fork can be built with or without it. With SLIC3R_CAD OFF
the build is behaviour-neutral against upstream OrcaSlicer; this is the
"parallel build" for upstream integration discussion.
Gated surface:
- option(SLIC3R_CAD) + add_definitions(-DSLIC3R_CAD)
- deps/OCCT/OCCT.cmake: BUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD}
(OFF matches upstream OCCT exactly; ON adds only TKFillet + TKOffset)
- TKFillet/TKOffset link, add_subdirectory(slvs) + libslvs
- CAD kernel + GUI sources, GLGizmoPrimitive (needs GeometryEngine),
CAD Catch2 tests
- 11 upstream C++ hook sites (GLCanvas3D, MainFrame, GLGizmosManager)
- TabPosition and gizmo EType enums switched to implicit numbering so
OFF reproduces upstream indices exactly
Verified on behemoth both ways: OFF and ON link snapmaker-orca +
libslic3r_tests (exit 0); ON shows the Design tab and passes 8 [design]
+ 1 [Deviation] tests, OFF omits them and shows upstream's tab layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
G-code post-processing is now a step of the slicing-pipeline plugin rather than a
separate capability type. One capability class can transform slices at the geometry
seams AND edit the final G-code, behind a single picker/option.
- Add SlicingPipelineStepPlugin::psGCodePostProcess (bound as
orca.slicing.Step.psGCodePostProcess). Unlike the geometry steps it fires from the
GUI export path in PostProcessor.cpp, not from Print::process(): ctx.print/ctx.object
are None and the plugin edits the file at ctx.gcode_path in place. It may run more
than once per slice (file export and/or upload) and its output is not shown in the
preview.
- Extend SlicingPipelineContext with gcode_path/host/output_name and a C++-only
full_config; config_value() falls back to it when there is no live Print.
- PostProcessor.cpp dispatches SlicingPipelinePluginCapability at psGCodePostProcess,
driven by the existing slicing_pipeline_plugin option.
- The exported G-code lives outside data_dir(), so the plugin audit sandbox would
block the write; the trampoline's audit setup grants ctx.gcode_path's folder as a
scoped allowed root, gated on a non-empty gcode_path so the geometry-step hooks gain
no extra filesystem access.
BREAKING CHANGE: the separate G-code post-processing capability type is removed.
- orca.gcode.GCodePluginCapabilityBase and orca.PluginType.PostProcessing are gone;
post-processing plugins migrate to orca.slicing.SlicingPipelineCapabilityBase +
Step.psGCodePostProcess (and gain ctx.params / ctx.config_value()).
- The post_process_plugin config option is removed; use slicing_pipeline_plugin.
Presets carrying the old key degrade to the standard unknown-key warning.
- Manifest type = "post-processing" now maps to Unknown (advisory only; the loader
dispatches on the C++ get_type()).
Also repairs two latent build breaks the branch carried: stale Step enum value usages
in test_slicing_pipeline_hook.cpp and a reference to the removed
ConfigOptionDef::PluginType::None in Tab::on_value_change (now is_plugin_backed()).
Adds the orca_gcode_stamp sample plugin and a psGCodePostProcess binding test.
The sidebar extruder cards get an interactive title row — "<name> ( <count> )"
with an edit button — showing the extruder's physical nozzle count on
multi-nozzle printers (hidden elsewhere). Clicking it opens the existing
"Set nozzle count" dialog, which now also handles a Hybrid extruder by
offering both Standard and High Flow counts (an empty mix is rejected) and
shows a hotend thumbnail.
Because `extruder_nozzle_stats` is session-only (saved presets never carry
it, so preset switches rebuild the edited config without it), the stats are
re-baselined whenever they are missing: each extruder starts with
extruder_max_nozzle_count nozzles of its selected volume type. Switching an
extruder's flow type carries its total count over to the new type, except
when the stats came from a device sync — the machine-reported per-type
breakdown must survive a manual flow switch.
The badge refreshes on preset load, flow-type change, manual edit, device
sync, and project load. Single-extruder cards keep the title row but never
enable editing.
An extruder with more than one physical sub-nozzle can hold a mix of
Standard and High Flow nozzles. The Flow dropdown now offers Hybrid for
such extruders (extruder_max_nozzle_count > 1, nil-guarded); grouping
already expands a Hybrid extruder into per-volume nozzle groups from
extruder_nozzle_stats.
- sidebar Flow combo offers Hybrid only for multi-sub-nozzle extruders
- preset lookup treats Hybrid as Standard (presets define no Hybrid
variant); variant strings are never fabricated for it
- printer tab splits a Hybrid extruder into Standard + High Flow rows,
with matching selection-index arithmetic and sync-enable rules
- syncing from a printer whose extruder holds mixed nozzle flows now
selects Hybrid instead of collapsing to the dominant flow type
- send-to-printer flow check: a nozzle-rack extruder validates its
nozzle inventory (mounted + rack) against every needed flow instead
of comparing only the mounted nozzle; mounted-flow lookup is now
per-extruder, fixing an index shift when a nozzle reports no flow
- Hybrid is session-only in app config (stored as Standard), so a
fresh session starts from concrete flow types
Printers whose extruders have a single sub-nozzle (including all
dual-extruder machines without a rack) see no new option and identical
check behavior.
Plugins dialog and the Speed Dial popup each
carried a private copy of the same fuzzy
matcher and had already drifted. Move the
pure functions into a shared module loaded
by both pages, and add a unit test beside
it.
Right-clicking a fav tile now opens a small
context menu with Move left, Move right, and
Unpin, instead of relying on drag-only reorder.
- speeddial.js: oncontextmenu handler, the
menu build/position/dismiss logic, and an
Escape-key close.
- style.css: .ctx-menu/.ctx-item styling.
- ActionRegistry: reorder_favourites persists
the new bar order to AppConfig.
- SpeedDialDialog: routes the new
reorder_favourites command from the page.
runnable was hardcoded true on every action,
and only loaded+enabled capabilities ever
reach the registry, so every JS === false
branch was unreachable. Drop the field, its
JSON, the guards, and the orphan .disabled
CSS rule.
Declare support_cooling_filter=1 on the three profiles (0.2/0.6/0.8
variants inherit from 0.4) and insert the cooling-filter conditional
into the H2D and H2S machine start g-code, inside the low-chamber-temp
airduct branch:
{if(cooling_filter_enabled)} M145.2 P0 F0 {else} M145.2 P0 F1 {endif}
H2D Pro intentionally gets no g-code edit: its duct firmware takes the
filter mode over the device channel only, so the flag merely enables
the toggle.
Impact on existing users at default settings: H2D/H2S start g-code gains
exactly one line (M145.2 P0 F1, filter off) in the cool-chamber branch;
nothing is removed or reordered. The existing support_air_filtration=1
overrides are deliberately kept so exhaust-fan behavior for ABS-class
filaments is unchanged, even though the machine-tab row is hidden while
the cooling-filter toggle is shown.
cooling_filter_enabled existed as a config option but was shown nowhere,
and there was no capability flag to gate it. The cooling filter and air
filtration are alternative accessories sharing the same duct, so a
printer declares one or the other.
- new hidden printer capability flag support_cooling_filter
- "Use cooling filter" toggle in the Accessory group, shown only when
the printer supports it; the air-filtration toggle hides in that case
(no vendor restriction: third-party printers keep air filtration)
- explicit defaults (0) in the common machine base
- H2C declares support_cooling_filter=1 instead of support_air_filtration;
its start-gcode already carries the cooling-filter conditional, so the
toggle is functional. On H2C this drops the two exhaust-fan lines that
air filtration emitted for ABS-class filaments, matching the printer's
actual duct accessory; H2C is new on this branch so no existing user
output changes.
Printers without the flag keep exactly the previous accessory UI and
g-code.
New printer option fan_direction (undefine/left/right/both, default
undefine) declares which side the auxiliary part-cooling airflow comes
from. When set and the printer has an auxiliary fan, auto-orient adds a
yaw rotation so the dominant overhang area faces the airflow, and newly
added primitive shapes are pre-oriented the same way (except the Cube,
whose axis-aligned bounding box the pressure-advance pattern calibration
depends on).
- FanDirection enum + fan_direction printer option (Accessory group,
enabled only with auxiliary_fan)
- orient engine: weighted overhang areas per candidate, yaw-direction
search, vertical rotation applied on top of the primary orientation;
the cooling weights are taken from the candidate actually chosen,
including the flat-bottom tie-break
- orient_for_cooling() for primitive placement
- set fan_direction=left on H2C/H2D/H2D Pro/X1/X1E/P1S 0.4 profiles
(X1C/H2S/P2S/X2D/Qidi X-Max 4 already carried the key, which now
takes effect)
With fan_direction unset or no auxiliary fan the vertical rotation stays
identity and auto-orient results are unchanged; slicing and g-code are
never affected.
The engine already implements prime_volume_mode (Default/Saving/Fast)
but nothing in the UI could set it, leaving prime-saving unreachable on
multi-sub-nozzle extruders and fast purge unreachable on printers that
support it.
- new PurgeModeDialog with selectable Standard/Fast or
Standard/Prime Saving cards depending on printer capability
- "Purge mode" sidebar button next to Flushing volumes; opens the
dialog and stores the choice in the project config
- printer preset-load gating: button shown only when the printer has
multiple sub-nozzles per extruder or sets support_fast_purge_mode;
stale project values the printer cannot honor reset to Default
- enable fast purge on A2L 0.4 (support_fast_purge_mode), explicit
default 0 in the common machine base
- new dialog strings added to OrcaSlicer.pot
Printers without these capabilities never show the button and their
projects keep prime_volume_mode at Default, so slicing output is
unchanged.
Fixes a regression where Timelapse and SD Card media failed to load on Bambu devices.
During the dual-cloud-agent refactor, NetworkAgent was updated to require an explicit provider argument. The call site in MediaFilePanel::fetchUrl() was missing this argument, causing it to silently fall back to the default Orca stub instead of routing to the Bambu network plugin.
This explicitly passes wxGetApp().get_printer_cloud_provider() to get_camera_url to correctly route the request and restore functionality.
Filament grouping already consumed per-filament forbidden nozzle volume
types, but every call site passed an empty map, so a variant-restricted
filament (e.g. one limited to "Direct Drive TPU High Flow") could be
auto-grouped onto an incompatible nozzle flow type on multi-variant
printers.
- add convert_to_nvt_type() to parse extruder variant strings
- add Print::get_filament_unprintable_flow(): forbidden volume types =
printer extruder variants minus the filament's declared variants;
filaments declaring no variants stay unrestricted
- feed the map into grouping at the by-object path (Print.cpp) and all
six mapping/planning sites in reorder_extruders_for_minimum_flush_volume
- unit-test the string parser
Non-restricted configurations produce an empty map, so existing
printers' grouping and g-code are unchanged.
Wire the search-first launcher's pure helpers to their node-vm spec:
resultCountText, selectedActionId, actionLabel (accessible row/tile labels
with plugin-key disambiguation), and tileCode (collision-escalating
title/pkg/ordinal monogram). Plus three fixes surfaced by the test + review:
- visibleFavourites drops non-runnable favourites - a dead fav tile
otherwise renders and run()s to a silent no-op on click.
- .fav-tile mirrors the list tile's inline-flex centering + 12px so a
multi-char monogram (e.g. "EA1") no longer clips the bare button.
- Arrow Left/Right stop swallowing the search caret in the list zone;
they only navigate when inside the favourites bar.
The previous guard only caught an empty glob. That is the wrong invariant.
The glob ships whatever the deps prefix holds, which is not the same as what
libslic3r links.
This fork sets BUILD_MODULE_ModelingAlgorithms=ON; upstream OrcaSlicer sets it
OFF. Upstream's DataExchange module pulls in most ModelingAlgorithms toolkits
transitively, but not TKFillet and TKOffset -- those two exist only because we
turned the module on. So a source build against a deps tree carried over from
upstream has 40 of the 42 OCCT DLLs. The glob copies all 40, the build
succeeds, and the slicer dies at launch with "error 126 (dependency not
found)". Reported by SoftFever, who named exactly those two DLLs.
CI is unaffected: the deps cache key is hashFiles('deps/**'), so flipping the
OCCT flag invalidated it and every shipped artifact has all 42.
Publish OCCT_LIBS from libslic3r as the single source of truth and check each
toolkit has a DLL, so the list can never drift from what we link. Verified
against a simulated stale prefix: the configure fails naming exactly
TKFillet.dll;TKOffset.dll, and passes when both are present.
Windows-only: every call site of the copy function is inside if (WIN32).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The glob that replaced the hand-maintained OCCT DLL list traded a loud
failure for a silent one. The old code named each DLL explicitly, so an
unpopulated deps prefix made file(COPY) error out at configure time. A glob
that matches nothing instead yields an empty list, copies nothing, and leaves
the install manifest without a single TK*.dll -- producing a package that dies
at launch with "error 126 (dependency not found)".
This only bites source builds whose CMAKE_PREFIX_PATH lacks bin/occt (OCCT's
install layout varies by version); CI populates it, so every shipped artifact
is intact. Restore the loudness rather than ship a slicer without OpenCASCADE.
Windows-only: every call site of the copy function is inside if (WIN32).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Romanian was absent from this fork and from upstream, so there was no
catalog to reuse. Machine-translated with a local qwen3.6 model against the
union of both forks' .pot files; 99.0% coverage (5472 singular + 9 plural).
Header credits it as machine translation pending native review.
Registering wxLANGUAGE_ROMANIAN in supported_languages[] is what actually
exposes the language: the Preferences dropdown filters the installed
dictionaries against that allowlist, so a valid .mo alone would ship a
language nobody could select.
Translations that failed to preserve their printf/boost placeholders were
left empty rather than shipped, since msgfmt --check-format is fatal in
run_gettext.sh and would break the build on all three platforms. Those 57
strings fall back to English. Verified: run_gettext.sh exits 0 and the
compiled catalog has 0 placeholder mismatches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The FilamentGroup property/golden harness checked a per-extruder
max_group_size cap unconditionally in check_constraints. That cap is an
invariant of the flush-partition solvers only (calc_group_by_enum /
calc_group_by_kmedoids, reached via calc_filament_group_for_flush), which
partition filaments subject to each extruder's capacity.
MatchMode (calc_filament_group_for_match) does not partition by capacity:
it maps every filament to the extruder holding the nearest-color loaded
AMS filament, and its solver capacity is the used-filament count, not
max_group_size (FilamentGroup.cpp:1067). So a legitimate MatchMode result
can place more than max_group_size filaments on one extruder.
The prop_a/b/c_mode_match specs run MatchMode, and their scenarios are
generated with std::uniform_int_distribution / std::shuffle, which are
implementation-defined. For a fixed mt19937 seed, libc++ (macOS), libstdc++
(Linux) and MSVC (Windows) draw different scenarios, so the CI failure only
surfaced on Linux/Windows while macOS passed. Verified locally: 248/600
config-A MatchMode seeds exceed the cap under libc++ — it is reachable
everywhere; seed 90400 just isn't an exceeding draw on macOS.
Gate section 3 on FGMode != MatchMode. No test case is removed or skipped:
all 57 FlushMode specs still assert the cap, MatchMode still asserts the
unprintable-filament/volume correctness constraints (which it honors), and
MatchMode grouping regressions are still caught by the golden score gate at
3% tolerance. Test-only change; slicing behavior and g-code are unaffected.
NfpPlacer stores std::reference_wrapper to the items it packs and re-reads
them from finalAlign() in its destructor (via clearItems()). Two placer
tests declared the placer before the items in the same scope, so the items
were destroyed first and the destructor dereferenced dangling references.
On macOS this is a deterministic SIGSEGV: libmalloc poisons the freed block
on free, so the item's point vector reads back as ~null (deref at 0x8). On
Linux/glibc the freed bytes usually survive, which is why it slipped through
upstream CI (introduced by #14267).
Declare the items before the placer so they outlive it, matching the pattern
the sibling 'packs many items' and 'obstacle' tests already use. Test-only;
the library lifetime contract (items must outlive the placer) is unchanged
and honored in production via _Nester in Arrange.cpp.
* Disable prime tower width for rib walls
Only enable the prime tower Width field when the selected wall type uses the standard tower shape. Rib towers use the rib-specific settings instead, so leaving Width editable suggests it changes rib geometry when it does not.
Fixes#14537
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify prime tower width toggle
Use the existing rib-wall helper when disabling the prime tower width control for rib wall towers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fix: prevent startup crash when preset-sync directory scan hits a transient FS error
On startup the user-preset sync thread scans the preset folder for orphaned
.info files (scan_orphaned_info_files). It iterated the directory with a
throwing boost::filesystem::directory_iterator while running on a background
thread that has no exception guard. On macOS, readdir() can intermittently
fail with ENOTSUP (errno 45); boost then throws filesystem_error, which --
uncaught on the sync thread -- calls std::terminate and aborts the whole
application on startup.
- Iterate with the error_code-based directory_iterator so a transient read
failure is logged and skipped instead of thrown. The orphan scan is
best-effort and re-runs on the next sync, so skipping a cycle is harmless.
This mirrors the existing pattern in has_json_presets() and the plugin scan.
- Wrap the entire sync-thread body in try/catch as defense-in-depth, so no
future uncaught exception on that otherwise-unguarded thread can abort the
app.
The Filament Track Switch (H2-series accessory, product code O2L-FTS) feeds
every AMS to both extruders through a two-track switch. Port full support
across the device layer, project config, and GUI.
Device / config:
- Model the switch-aware AMS binding (the set of extruders an AMS can feed
and which input track A/B feeds it), switch readiness, the O2L-FTS firmware
module, and the fun2 capability bit for checking a slice against installed
hardware.
- Register has_filament_switcher and enable_filament_dynamic_map as project
config that persists with the project and restores from a saved 3mf, and
force both back to false on every project/printer/CLI load path. Live
device sync is the only thing that sets them true.
GUI:
- Sidebar sync activates the switch from live device state, attributes each
AMS to the extruder its input track feeds, shows a floating status icon
(ready / not-calibrated), and surfaces a one-time tip / not-calibrated
warning.
- Send dialog gains a non-blocking slice-vs-hardware mismatch warning and a
blocking error when a slice needs dynamic nozzle mapping but the switch is
missing or not set up.
- AMS load/unload guards, AMS-view routing glyph + un-calibrated banner +
hidden external-spool road, and mapping-popup external-spool lockout.
- Filament pickers collapse the per-extruder split into a single deduplicated
"AMS filaments" group with a smart-assign toggle when the switch is ready.
- Firmware-upgrade panel lists the O2L-FTS accessory and its version.
- Device-provided filament-change steps (ams.cfs) drive the change-step
display when firmware sends them, including the three switch steps.
- "Load current filament" asks which extruder to feed via a
FeedDirectionDialog when the switch is calibrated.
Inert without the accessory: every path is gated on the switch being
installed (MQTT aux bit 29, default off) or ready, both project flags default
false, and the per-extruder AMS attribution is byte-identical, so AMS state,
the send/load UI, and sliced g-code are unchanged for every printer that does
not report a Filament Track Switch.
* fix: out-of-bounds read computing tool-ordering max layer height
calc_max_layer_height() loops over the extruder count (nozzle_diameter)
but indexes max_layer_height with the same counter, reading past the end
when that array is shorter. Silent on release builds, aborts under a
bounds-checked STL (_GLIBCXX_ASSERTIONS).
Read via get_at(), which falls back to the first entry when the index is
out of range, as Slicing.cpp already does for this option.
Add a fff_print regression test slicing a two-extruder printer with a
single-entry max_layer_height.
* docs: clarify how max_layer_height ends up short in the regression test
Normalization sizes it to the filament count under single_extruder_multi_material,
not "a mismatch a profile can ship" as the earlier comment guessed.
round() already had unit coverage; floor() and ceil() had none. Add the missing
positive cases for both signs, plus round()'s half-away-from-zero tie-break, and
one negative case asserting that a name outside the grammar's built-in function
set is treated as an undefined variable and throws, rather than being passed
through to a math library.
GCode::extrude_support declared its per-path speed helper as a function-local
static lambda that captures `this` by reference. The closure is built once, on
the first extrude_support call, and reused for the rest of the process, so a
second G-code export in the same process runs the helper against a `this` from
the first export's stack frame, which has already returned.
The stale `this` flows through NOZZLE_CONFIG(...) -> cur_extruder_index() ->
GCodeWriter::filament(), reading a garbage current-extruder id and indexing
with it. It is silent whenever the reused stack still holds a usable pointer,
and an order-dependent SIGSEGV otherwise; AddressSanitizer reports it as a
stack-use-after-return in GCodeWriter::filament(). It is the only static
capturing lambda in libslic3r.
Drop static so the closure is rebuilt each call against the live frame. Add an
fff_print regression test that slices a support object twice in one process; it
fails without the fix (stack-use-after-return under ASan) and passes with it.
Multi-nozzle sync widget, AMS rack-nozzle mapping popup, calibration rework, send-dialog nozzle mapping and extruder-count UI. Includes the fix to persist the AMS sync badge on filament cards (H2C/A2L and direct-sync printers).
Nozzle rack data model and device-tab panel, multi-nozzle sync, per-nozzle filament blacklist, and print-dispatch nozzle mapping (DevNozzleMappingCtrl V0/V1).
* ci: run unit tests on Windows and macOS
The Unit Tests CI job only ran on Linux, so platform-specific bugs
invisible to a Linux build could land undetected (e.g. the MSVC-only
NfpPlacer crash fixed in #14267). The full non-[NotWorking] suite already
builds and passes on every shipped arch, so this wires them into CI.
A reusable unit_tests.yml, called once per built arch, downloads that
arch's test artifact and runs ctest. Each build leg builds the test
executables and uploads them; a single publish_test_results job on Linux
aggregates the JUnit results into one check.
Coverage: Linux x86_64 + aarch64, Windows x64 + arm64, macOS arm64.
macOS x86_64 is deferred (cross-built on arm64, needs Rosetta).
- build_release_vs.bat: "tests" token enables BUILD_TESTS
- build_release_macos.sh: -T builds and runs tests; ORCA_TESTS_BUILD_ONLY
builds them without running (used by CI)
- scripts/run_unit_tests.sh: parameterized test dir and build config
Addresses #11273.
* ci: bump actions/checkout to v7 in reusable unit_tests workflow
Match the actions/checkout v6->v7 bump (#14517) that upstream applied to
the inline test job this reusable workflow replaces.
* fix: isolate calibration temp paths per user
The calibration temp files under <temp>/calib were file-scope statics
initialized before set_temporary_dir() runs at startup, so they kept
using the shared system temp root and missed the per-user isolation
added in #14607. On Linux every account shares /tmp, so the first user
to calibrate owns /tmp/calib and later users fail to write there, the
same cross-user collision #14607 fixed for model backups, STEP import,
and part skip.
Build the paths lazily from temporary_dir() instead, through a
calib_temp_dir() accessor and a calib_temp_file() join helper. The base
becomes <temp>/orcaslicer_<uid>/calib on Linux and is unchanged on
Windows, where the temp dir is already per user.
Also make StoreParams::path a std::string rather than a non-owning
const char*. The calibration code had to keep a std::string alive
solely to feed that pointer, and the field was uninitialized by
default; owning the string removes the lifetime hazard for all three
callers and makes the entry guard a reliable empty() check. Confined to
the 3mf project exporter (three callers, two internal reads); no
on-disk, format, or ABI impact.
Follows up on #14607 per @Noisyfox's review suggestion.
* dedupe-issues.yml remove unsupported claude_args option
claude_args: is causing an error. Only calling for "--model" anyway, so replaced with model: option.
* Merge branch 'main' into MisterAnderson91-dedupe-issues-yml-update
* fix: prevent out-of-bounds crash in Arachne beading interpolation
SkeletalTrapezoidation::interpolate() derives an inset index from `left` but
uses it to index the merged beading, which follows the thicker of left/right.
When the thicker side has fewer insets, the index runs past the end and the
slicer crashes during "Generating walls".
Skip the adjustment when the index is out of range, as the adjacent guards
already do. interpolate() uses no instance state, so make it static and add a
regression test that exercises it directly.
Fixes#14584
Replaces the plugin-only set_slices/set_fill_surfaces/set_lslices mutators with a
faithful, mutable binding of the core geometry types, so a plugin edits the slicing
graph through the same object model the C++ code uses.
- Point, Polygon, ExPolygon, Surface and SurfaceCollection gain constructors,
writable accessors (contour/holes, set/append/clear, filter_by_type), transforms
(rotate/scale/translate), boolean ops and offset. Polygon exposes a zero-copy
writable numpy view via a make_writable_rows helper.
- LayerRegion.slices/fill_surfaces stay read-only refs but are now live,
in-place-editable SurfaceCollections; Layer.make_slices() re-derives the islands
and refreshes lslice bounding boxes.
- Rewrites the Inset and Twistify samples on the new API (in-place ExPolygon
transforms, ExPolygon.offset, SurfaceCollection.set), dropping their numpy
dependency; each touched layer calls make_slices() so downstream steps see the
edited footprint. Adds tests covering in-place edits through a live collection.
BREAKING CHANGE: set_slices/set_fill_surfaces/set_lslices and the internal
parse_expolygon(_list)/surfaces_from_py helpers are removed. Plugins mutate through
the class API (SurfaceCollection.set/append/clear, Polygon.set_points/append,
ExPolygon.set_holes) instead.
* Support Painting: add Vertical/Horizontal axis-lock checkboxes
Adds the Vertical and Horizontal axis-lock checkboxes to the Support
Painting gizmo, matching the UI in the MMU Segmentation and Seam
Painter gizmos. The underlying constraint logic has lived in
GLGizmoPainterBase since #2424 and already applies to any
ToolType::BRUSH action — the Support gizmo was the only painter
without the UI to enable it.
The Circle and Sphere brush arms are consolidated into a single
"if (Circle || Sphere)" block matching the structure of
GLGizmoMmuSegmentation::on_render_input_window, eliminating
duplicate cursor-radius and axis-lock UI code.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Support Painting: keep Circle/Sphere as separate tool arms per review
Reviewer requested keeping a separate condition per tool for future
extensibility rather than merging Circle and Sphere into one branch.
This restores the upstream Circle/Sphere arm structure and adds the
Vertical/Horizontal axis-lock options to each arm, making the change
purely additive over upstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: yw4z <ywsyildiz@gmail.com>
feat: add regex_replace() string transform to filename templates
The filename template language could test strings (=~, !~, one_of) but
never rewrite one, so there was no supported way to reshape a placeholder
value, such as dropping a file extension from {first_object_name}.
Add regex_replace(subject, /pattern/, replacement), reusing the existing
regex-literal syntax and boost::regex engine. Every placeholder keeps
returning its exact value and the template does the transform explicitly:
{regex_replace(first_object_name, /\.[^.]*$/, "")} strip any extension
The replacement may reference capture groups ($1, $2, ...). It is one
grammar function mirroring digits(), with the name registered as a keyword
so it is not parsed as a variable.
Brings in the Plugins dialog as-you-type search with fuzzy match highlighting
and clickable column-header sorting (name, version, source, status) — PRs
#14610 and #14611.
Adds PluginHostSlicing, which registers the print-graph data model (Print,
PrintObject, Layer, LayerRegion, Surface, ExPolygon, extrusions, ...) into the
orca.host submodule in the same raw-class style as PluginHostApi's Model/Preset
graph, with shared helpers in PluginBindingUtils. SlicingPipelinePluginCapability
is trimmed to the capability surface (the standalone SlicingNumpy helper is folded
away). Adds the Twistify example plugin next to Inset and broadens the binding,
hook, and plugin-install tests.
Port from BambuStudio commit c8f70c6ca. Shows a warning when AMS
is actively drying during print job preparation, alerting users that
the drying temperature will be lowered during printing.
Three-page wizard: main status/control page, guide page with
filament tray status, and progress page. Supports N3F and N3S AMS types.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: initialize Print::m_isBBLPrinter
Built outside the GUI/CLI (headless tests, embedded use) the member was read
uninitialized: is_BBL_printer()/wipe_tower_type() feed it into ToolOrdering,
which then non-deterministically dropped per-feature filament assignments.
Default it to false, the value the GUI and CLI already assign for non-Bambu
printers.
* docs(test): add the fff_print testing contract
tests/fff_print/README.md codifies how the suite is organized: one file per
subsystem (each owning both in-memory and emitted-G-code assertions), flat
behavioral test names with a single [Subsystem] tag, a robust-tests guide,
the shared helpers, and an add-a-test checklist. Linked from tests/CLAUDE.md.
* test(fff_print): reorganize the suite to the contract and add coverage
Bring every subsystem into one file per the README: rename the test_data
harness to test_helpers; consolidate skirt/brim; split multi-filament and
cooling into their own files; disperse the test_printgcode grab-bag and the
end-to-end smoke scenario into focused tests; fold test_gcode into
test_gcodewriter. Standardize names and tags, align cube tests on the cube()
helper, and de-qualify the flagship files.
New coverage: multi-filament per-feature and per-object routing; a skirt/brim
behavior matrix (the #14333 rework, including brim ears, with regression
coverage for #14319 and #14366); resolved extrusion-width and config
comments; custom-G-code placeholders; fan control and speed-marker
consumption.
Re-enable three slice tests previously tagged [NotWorking]: the clipper
"Coordinate outside allowed range" error that disabled them was specific to a
past CI runner environment and no longer reproduces.
* test(fff_print): tag arm64-flaky skirt/brim tests NotWorking
Four skirt/brim slice tests intermittently throw ClipperLib's "Coordinate
outside allowed range" on the macOS and Windows arm64 CI toolchains (an FP
divergence, not a slicing bug; see PR #14207). Linux x86_64 and aarch64 are
unaffected. Tag them [NotWorking] so ctest -LE NotWorking skips them.
* test(fff_print): re-enable the arm64 skirt/brim tests
These were tagged [NotWorking] as a stopgap when myfork's daily-driver build
combined them with the cross-platform CI on a base that predated upstream's
m_origin fix (99dea01cc3). With upstream merged in, Print::m_origin is
initialized and the "Coordinate outside allowed range" throw is gone, so the
tests pass on macOS/Windows arm64. Drop the tags.
Replace the toolbar sort dropdown with sortable Name/Source/Status column
headers that cycle ascending, descending, then clear. Add a Source column
and a PluginSortKey::None baseline for the cleared state. The whole header
cell is the click target and the sort triangle snaps in without a fade.
fix(gui): avoid null-pointer UB in create_scaled_bitmap with win == nullptr
create_scaled_bitmap() documents that win may be nullptr, but called
win->FromDIP() on it. Calling a member function through a null pointer
is undefined behavior: clang assumes `this` is non-null and deletes the
subsequent `win ?` null check added in #13117, turning the fallback
branch into an unconditional virtual call through a null vtable.
This crashed LLVM/clang-cl builds at startup (access violation reading
0x0 in BBLTopbar creation); MSVC builds were unaffected by luck.
Use the static, null-safe wxWindow::FromDIP(x, win) overload instead,
which falls back to the primary display DPI. Behavior is unchanged for
non-null windows.
On Linux every account shares /tmp, but slicing builds temp paths there under
fixed, app-owned names via temporary_dir() (model backups, STEP import,
part-skip). The first user to slice creates and owns those dirs, so the next
user cannot write under them and slicing crashes with "No such file or
directory".
Tag the app temp root with the user id at startup (<temp>/orcaslicer_<uid>)
so every temporary_dir() consumer is isolated at once. The id stays at the
top level of the world-writable system temp so each user's dir is created
directly there; a shared parent dir would be owned by whichever user made it
first. The root is pre-created because STEP import writes into it directly.
Windows keeps the plain temp dir since it is already per-user.
Fixes#10108. Same root cause as #5969.
* fix profile reference for Creality
* fix profile reference for Blocks
* fix profile reference for OrcaArena
* fix profile reference for re3D
* fix profile reference for Chuanying
* fix profile reference for Prusa
* fix profile reference for Wanhao France
* fix profile reference for MagicMaker
* fix profile reference for Afinia
Remove the ABS/ABS+/PLA/TPU/Value ABS/Value PLA filament presets that referenced the non-existent "Afinia H400 Pro" printer. The real printer is "Afinia H+1(HS)", already served by the @HS filament variants.
* fix profile reference for Comgrow
Remove the orphaned "0.20mm Standard @Comgrow T500 1.0" process preset and its process_list entry. Its only compatible printer "Comgrow T500 1.0 nozzle" never existed (the T500 model defines nozzle diameters 0.4/0.6/0.8 only).
* always run check_preset_references
The 409 conflict notification, the force-push confirmation dialog, and the payload-too-large (413) dialog now name the affected preset. The name was already parsed from the conflict body but never surfaced. The account-level preset-limit message stays generic since it isn't about one specific preset.
* Fix reload from disk for STEP models after reopening a project (#12992)
reload_from_disk matched reloaded source volumes with an exact
source.input_file string comparison. After a project is saved and
reopened, the stored source path is only the filename (the default,
non-full-path save) while a freshly re-imported volume carries a full
path, so the comparison never matched: reload fell into fail_list and
the "locate file" dialog was effectively useless for STEP models.
Fall back to a case-insensitive filename comparison when the exact
paths differ, so the existing same-folder source lookup (and the
locate dialog) can reload the model. Projects that stored absolute
source paths still match exactly as before; no 3mf format change.
* Add Preferences option to store full source paths in projects
Expose the existing export_sources_full_pathnames setting (previously
only editable in the config file) as a checkbox under Preferences >
General > Project. Enabling it stores absolute source paths in saved
projects, so "Reload from disk" works when the source file is kept in
a different folder than the project (companion to #12992).
Every remaining legacy filament_id outside the BBL/QD_* islands re-derives
from its product triple (filament_vendor / filament_type / family name),
completing the content-addressed id model of filament_id_plan_v3.md:
- Relocation pre-step: 14 fdm_filament_* template forks (Cubicon x3,
Prusa x6, RH3D x6 minus the pc fork already re-homed) stop declaring
ids; the 16 presets that rode Prusa's forks now declare the id they
already resolved (verified zero effective-id drift over all 5892
instantiated presets).
- --drop-redundant-ids: 6 Custom/MyToolChanger generics drop copied GF
ids and ride their OFL families.
- --remint over all 62 non-island vendors: 2647 declarations re-derived;
identical products converge cross-bundle (showcase: PolyLite PLA is now
OF5CgdDq in OrcaFilamentLibrary, Qidi, OrcaArena and Snapmaker).
- Prusament @XL completions surfaced by the relocation: filament_vendor
["Prusa Polymers"] on the 8 @XL declarers; Prusament PA-CF typed
PA11-CF (the product is Prusament PA11CF) and PC-CF typed PC-CF
(-CF family typed as base polymer); each family converges on one id.
- Succession: 331 ids retired with mode-rule successors, 355 never-shipped
v1 mints forgotten with chain splicing, 151 GF-shaped ids released to
the island space with hints, 27 curated BBL-generic -> OFL-generic
hints (GFL99 -> OFDSrzZ8 class). New --retire "OLD=NEW" maintenance
mode records lineage for OGFC99/OGFG99/OGFN99 (the shipped ids of OFL's
Generic PC/PETG/PA in released versions, whose claims migrated in v3.1
while Cubicon's inert fork declarations kept them alive).
- Retiring the P-hex system ids removes the last system ids from
check_ams_filament_valid's destructive P-gate.
- AMS-ambiguity fixes surfaced by convergence (validator -f): 4 presets
riding another family's id through inherits now declare their true
family id (Elegoo Generic ASA-CF/PETG-CF, Snapmaker PolyLite Dual PLA /
PolyLite J1 PLA); 9 Cubicon @base presets and Dremel Generic PLA, which
duplicate their per-printer variants on the same printers, split onto
the salt-1 iteration of their triple (sanctioned by the mint-conformance
check; --remint now leaves salt-conformant declarations alone).
Gates: assign_filament_ids --check 0; orca_extra_profile_check 0;
116 python unit tests; config-equivalence over all 5892 instantiated
presets (byte-identical configs except the 14 sanctioned Prusament
vendor/type corrections; setting_id and compatible_printers unchanged;
per-family convergence and ledger conservation verified); profile
validator base/-f/-r(BBL)/-r(Qidi) all green; libslic3r_tests 48610
assertions; Moonraker OFL generic map check; custom-preset fixture
archives v1.9.0-v2.4.1.
Phase v3.1 of filament_id_plan_v3.md: every OFL-declared filament_id
re-derives from its product triple; the succession ledger absorbs the old
ids. Config-equivalence verified: flattened effective configs differ ONLY
in filament_id (284 consistent old->new changes tree-wide, including every
vendor preset that rides an OFL family), zero config/compatible_printers/
setting_id drift.
Structural pre-step (id-value-neutral, verified): OFL generic family ids
move from the shared fdm_filament_* template bases onto the product-named
"Generic X @System" presets, so the v3 triple's family component is the
product name, not an internal file name (12 relocations; 2 dead duplicate
declarations on fdm_filament_pa/pet pruned; every consumer chain itemized
first - the +12 instantiated_with_id entries are this deliberate pattern,
matching the 11 generics that already declared on @System).
fdm_filament_pc keeps its declaration this phase: 7 Prusa presets (Prusa
Generic PC, Prusament PC Blend) inherit it directly and re-home in the
v3.2 Prusa worksheet; its transitional id is documented in the plan.
Re-mint: 301 declarations re-derived (e.g. Generic PLA OGFL99 ->
OFDSrzZ8 = mint("filament_product/Generic/PLA/Generic PLA")). Snapshot:
1477 ids (+300/-292). Ledger:
- 230 shipped ids retired with mode-rule successors (OGF* library ids,
OFLSBS99, DREMC/FILAR/AliZ/eSUN/... legacy strings). DREMC010 had been
shared by a PPA-CF and a TPU family - a data bug this split resolves;
its successor follows its only shipped claim (DREMC PPA-CF).
- 25 GF-shaped ids OFL had copied from the Bambu catalog (GFOT00x
Overture, GFSEP0xx) are RELEASED to the BBL island space with hints at
the re-minted families - never retired, so a future legitimate BBL
catalog addition is never blocked; plus an explicit GFOT001 ->
OFxA1p01 hint (Overture PLA Pro, still live in BBL).
- 37 never-shipped v1 branch mints dropped from lineage
(--forget-never-shipped; they exist in no release, no forwarding
needed).
- OGFC99/OGFG99/OGFL96/OGFN99/OGFSNL08 stay live (still declared by
vendor bundles); they retire in v3.2 when those declarers re-mint.
Gates: 106 unit tests OK; --check exit 0; orca_extra_profile_check exit 0;
validator -l 2 / -f tree-wide / -r BBL+Qidi exit 0; Moonraker OFL map
check resolves all 29 aliases against the re-minted presets; flatten
equivalence pre/post as above; snapshot regen idempotent.
Implements phase v3.0 of filament_id_plan_v3.md - tooling and client
prerequisites. No filament_id values change in this commit.
Tooling (scripts/assign_filament_ids.py; 106 unit tests):
- The mint key becomes "filament_product/<filament_vendor>/<filament_type>/
<family_name>", resolved loader-faithfully from the declarer's flattened
config - bundle-independent and content-addressed; identity fixes re-id
by design and are made safe by the succession ledger. --mint now takes
the triple.
- The snapshot gains "triples" and "triple_exceptions" sections, folded
into the equality gate: any vendor/type/name change surfaces as a
reviewable snapshot diff. Check 3 rewritten to triple-mint conformance
with (id, triple) grandfathering; check 5 generalized from OFL generics
to every OFL-riding vendor preset; new check 8 (triple integrity) and
check 9 (succession integrity).
- The retired ledger moves to resources/profiles/retired_filament_ids.json
so it ships with the app; schema {claims, successor} plus cross-island
"hints". The 14 v1 entries are migrated with mode-rule successors.
Vanished ids in a foreign island's space (GF*/QD_*) are RELEASED with a
hint instead of retired: the island catalog owns them and may
legitimately (re)ship them, which check 4 must never block.
- New maintenance modes: --remint VENDOR, --drop-redundant-ids VENDOR,
--add-hint "OLD=NEW", and --update-snapshot --forget-never-shipped FILE
(never-shipped ids drop from lineage; chains splice through them).
Runtime (C++):
- Succession helpers in libslic3r/Preset (pure chain-follow + lazy ledger
load from resources), consulted only on resolution miss in
get_filament_by_filament_id, both AMS sync predicates,
add_ams_filaments, setting_id_to_type, and the calibration-history
lookup; behavior is byte-identical while the ledger has no matching
entry. Catch2 coverage in tests/libslic3r.
- W1 hardening: check_ams_filament_valid no longer remote-wipes trays or
rewrites temps for P-shaped ids that a system preset carries (ten such
system ids ship today); the size==8 && [0]=='P' assert is relaxed; the
unguarded filament_list find deref in the temp-equation check returns
non-destructively on a miss.
- MoonrakerPrinterAgent: the 23 hardcoded OFL generic ids are replaced by
a runtime lookup of "Generic <family> @System" (id-equivalent on
today's shipped profiles, follows future re-mints automatically);
scripts/test_moonraker_lane_data.py derives its expectations from the
shipped profiles and gains --check-ofl-map.
Profile data (W3 - type is now a key component, so type bugs are fixed
before any re-mint):
- 17 same-name-different-type groups corrected across 50 files (OFL
Generic PETG-CF/PE-CF/PP-CF; Flashforge ASA Basic/ASA-CF/ABS-CF/HIPS/
PAHT-CF/PLA Silk; FusRock PAHT; Creality Generic PA6-CF; InfiMech PETG;
Anycubic TPU 95A / TPU for ACE; Prusa Generic PA-CF/PLA-CF) - every
value validated against MaterialType::all(); 3 Snapmaker U1 roots gain
their missing filament_vendor. Flattened-config equivalence vs the
previous commit: exactly the 54 intended diffs, zero BBL.
- doc/developer-reference/filament_id.md rewritten for the v3 rule.
Ambiguous type divergences (Prusa Generic TPU/TPU HF FLEX-vs-TPU, FusRock
S-Multi/S-PAHT) and all same-type vendor-tag divergences are deferred to
the v3.2 vendor worksheets, catalogued with analysis.
Revises the v1 mint rule after the maintainer catch that its key scoped
families to the profile bundle (printer brand), fragmenting one commercial
product into N ids (PolyLite PLA: five bundles, all filament_vendor
"Polymaker", up to five ids).
Architecture: BBL and QD_* stay frozen islands; OrcaFilamentLibrary becomes
the single declaration point for every other material family; vendor
bundles carry only same-alias specializations (no filament_id key) that
shadow the OFL preset per printer and resolve its id through the loader
walk. New mint key: uuid5 over
"filament_product/<filament_vendor>/<filament_type>/<family_name>" from the
root's flattened config — bundle-independent (hoisting families into OFL is
id-stable), and the type component keeps the four known same-name-
different-type groups apart until their data bugs are fixed.
Content-addressing replaces "ids immutable once shipped": identity edits
re-id the family, made safe by turning retired_filament_ids.json into a
shipped succession ledger (old id -> successor, chains allowed,
cross-island hints permitted) consulted on resolution miss in the AMS sync
and lookup paths. The MoonrakerPrinterAgent hardcoded generic-id map is
replaced by a runtime preset lookup, removing the code/profile lockstep.
Migration: v3.0 tooling + W1 client hardening + W3 type fixes; v3.1 OFL
re-mint with succession entries; v3.2 vendor bundles (391 unshipped OF ids
re-derive without retirement; generic tunings re-point to OFL; ~800 shipped
legacy ids re-mint with succession, incl. the 57 multi-vendor GF residue
and the 10 P-hex system ids, which also exits them from the destructive
check_ams_filament_valid P-gate); v3.3 optional id-stable consolidation
into OFL. Gate battery unchanged plus succession-resolution tests.
filament_id_plan_v2.md gets a status note: its P+md5 verdict stands; its
work items are absorbed or superseded by v3.
Introduces a plugin capability that runs Python at the seams of Print::process(),
letting a plugin read and rewrite slicing state as it is computed.
- New slicing_pipeline_plugin config option; selected plugin refs are serialized
into the print manifest.
- Print gains an injectable hook fired at each pipeline step (posSlice,
posPerimeters, posInfill, ...). It is a no-op when unset, fires only on genuine
(re)computation, and never on the use-cache path.
- orca.slicing submodule: SlicingPipelineCapabilityBase plus a trampoline and a
Step enum. Capabilities read the live graph through zero-copy int64 numpy views
(contour/holes geometry with unscaled coordinates, flattened toolpath data) and
edit it through 2D-geometry mutators with cache-invariant refresh.
- GUI dispatcher runs capabilities during slicing under the GIL, turns plugin
errors into slicing errors, honors cancellation, and adds the plugin picker.
- Ships the InsetEverySlice sample plugin and binding/hook tests.
# 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?
-->
# 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.
-->
<!--
> 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)
* fix: crash in Measure tool when a plain edge is the first selection
The SPHERE_2 gripper raycaster called get_feature_offset() on
.first.feature instead of .second.feature (copy-pasted from the SPHERE_1
block). Plain planar-border edges store no extra point, so the Edge
branch dereferenced an empty optional behind a release-stripped assert,
aborting on Flatpak and undefined behavior elsewhere.
Point the SPHERE_2 raycaster at .second.feature and fall the Edge branch
back to the edge midpoint.
Fixes#14018
Examined minting system filament_ids with CreatePresetsDialog.cpp:487's
user-custom allocator (adopt-by-base-name, else "P"+md5(name)[0:7]) for
Bambu AMS-sync compatibility. Verdict: keep the OF* mint; capture the
proposal's value through bounded work items instead of a re-mint.
- The device path never validates id shape: tray_info_idx is an opaque
string end-to-end (DeviceManager.cpp:1642; DevFilaSystem.cpp:512-514)
and firmware persists arbitrary bytes (bambulab/BambuStudio#5436).
Resolution is by value against the GF catalog plus the account's cloud
custom cache; unknown ids show "?" regardless of shape, and system
presets can never enter that cache (Preset.cpp:2071 gates upload on
is_user()).
- The only id-shape dispatch in the tree is destructive for P-shaped ids:
check_ams_filament_valid (DeviceManager.cpp:5335/5352/5395/5410) remotely
clears an AMS tray (:5345) or rewrites its temps when a P-shaped tray id
drops out of the user-root preset list — a state name-keyed minting
creates by construction. GF* and OF* ids are structurally immune.
- The real value is captured without re-minting 391 unshipped ids across
1327 files: the adopt step == curated GF adoption via the snapshot ledger
(W4); the hash belongs to user presets (W5, cf. PR #13315); client
hardening (W1) that the ten already-shipped P-hex system ids (Cubicon x8,
Ginger, Artillery) need today anyway.
Method: 6 parallel evidence agents (generator semantics, AMS/device path,
exhaustive shape-dispatch sweep, upstream + online sources, tree-wide
dry-run over 5892 presets / 1148 base names, branch change inventory) into
a 3-judge panel (keep-OF* won 23/40 aggregate); the upstream generator was
verified byte-identical to BambuStudio master, and every load-bearing
file:line and number in the document was re-verified against the tree
before commit.
The portable/installer hardcoded an OCCT DLL list that drifted from what
libslic3r actually links (OCCT_LIBS) + their transitive OCCT deps. The
CAD build links TKFillet/TKOffset/TKBool (and pulls TKFeat/TKBin*/TKIGES/
TKRWMesh/TKSTL/TKVRML/TKXDEIGES/TKXml* transitively) which were absent
from the shipped zip -> OrcaSlicer.dll failed at launch with error 126
(dependency not found). Replace both hardcoded lists (the file(COPY) and
the install manifest) with a glob over the built occt/ dir so the package
can never drift from the build again. Windows-only: OCCT is Shared solely
on Windows (deps/OCCT/OCCT.cmake); macOS/Linux static-link it. Reported by Marc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Hole: with no face explicitly picked, the tool fell back to the XY datum at
z=0 (the model's underside), so placing a hole from a top view read parallax-
shifted. Default to the solid's top face (top_face_index_of) so the footprint
sits on the surface being viewed; the XY/XZ/YZ dropdown still overrides.
Internal thread: the bore was re-cut at the nominal radius, which coincides
with an existing hole's wall — the coincident faces fouled the groove boolean
so it removed ~nothing (invisible thread). Cut the bore at the minor diameter
(radius - depth) instead: strictly inside any existing wall, leaving it clean
for the groove; on solid stock it forms the tap-drill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
With every vendor's filament_id collisions fixed (356 collision groups /
1256 printer-level ambiguity errors across 30 vendors, plus the 16
library-internal ids and the 10 live library-cross groups), the
duplicate-filament-subtype validation no longer needs the BBL-only scope:
drop -v BBL from the -f step so any new ambiguity in any vendor fails CI.
Local verification of the full workflow against this branch:
- extra JSON check exit 0; validator -l 2 exit 0 (66 vendors);
-f tree-wide exit 0 (with the library-aware extended validator, which is
strictly stricter than the released binary CI downloads); -r exit 0 for
BBL and Qidi.
- custom-preset fixture archives v1.9.0..v2.4.1 overlaid per the workflow:
six pass outright; v2.3.1/v2.3.2/v2.4.0/v2.4.1 fail locally only on a
pre-existing Windows-only validator limitation (a user preset with a
non-ASCII filename reads as empty; reproduced byte-identically on the
pre-migration base commit, and absent on CI's Linux runners where these
archives validate green).
The custom-preset fixture validation (check_profiles.yml step 5, run locally
against the v2.3.2/v2.4.0/v2.4.1 archives) caught a real regression in the
previous Cubicon fix: those archives contain user presets whose inherits
names the @base presets directly ("can not find parent Cubicon ABS @base").
A system preset with instantiation:false is not added to the preset
collection, so flipping the nine @bases was equivalent to deleting their
names - exactly the user-preset drop hazard the migration rules forbid.
Repair: restore all nine @bases byte-identically to their pre-flip state
(instantiated, setting_id, original P510cf* ids; Cubicon PC @base keeps its
minted OFnLnZQo from the copy-paste fix). The AMS ambiguity is instead
resolved Dremel-style: each family's three passthrough variants share one
fresh variant-tier mint as own keys, so on every printer the base and the
variant carry different ids. Zero preset-set change, zero config change,
user presets and fixtures resolve exactly as before the migration.
Verified: fixture archives v2.3.2/v2.4.0/v2.4.1 no longer report missing
parents; config-equivalence holds; orca_extra_profile_check.py exit 0;
assign_filament_ids.py --check exit 0; validator -l 2 exit 0; tree-wide
extended -f still exit 0.
Phase 2 completion - Volumic, Ratrig, Chuanying, Afinia, Eryone, FLSun,
Tiertime, Blocks, CONSTRUCT3D, Co Print, CoLiDo, DeltaMaker, Ginger
Additive, OrcaArena, Peopoly, Wanhao France, iQ. All are P1 copy-paste or
small structural fixes; owners keep their ids by Bambu-catalog/OFL/historic-
introducer precedence, impostor families get fresh deterministic mints,
zero effective-config change:
- Volumic 10 mints (ABS/ASA/PP, PETG/PCTG/ESD, PLA/UNIVERSAL, PA/PPS lines);
Ratrig 10 (BigNozzle/PunkFil lines off the Generic line's ids); Chuanying
8 (plus 3 redundant own-id lines removed so parents' minted ids flow to
the 0.25-nozzle variants); FLSun 4 (S1/T1 High Speed + Silk); Peopoly 4
(Lancer lines off Generic PLA's GFL99); Afinia 3 (Value PLA/ABS+/Value ABS
inside the frozen GFx##_## scheme); Co Print 3 (ABS/PETG/TPU off GFL99).
- Tiertime: Generic SBS had copy-pasted Generic PLA's ids on both printer
lines - one family mint on both variants collapses the split. Eryone: PP
copy-pasted PETG-CF's EFL43 (same introducing commit) - PP minted.
Blocks: ASA-CF copy-pasted PLA-CF's BSFI010 - ASA-CF minted. CONSTRUCT3D:
High Flow PETG minted off GFG99. CoLiDo: both claimants of the invented
GFA99 re-minted. DeltaMaker: Brand PLA minted off GFL99. OrcaArena:
Generic PLA Silk minted; the Bambu-clone Arena PLA Silk keeps GFA05
(catalog precedence). iQ: Grauts HPP4GF25 minted off Fiberthree's IQM1.
- Ginger Additive (P5): the shared fdm_filament_common template carried
P510eff9; moved onto the two pellet families (Generic PETG keeps it as
first claimant, Generic PLA minted), template key removed after verifying
every inheritor still resolves an id.
- Wanhao France (P4): YUMI PLA Bowden stops over-claiming the six
direct-drive printers covered by YUMI PLA Direct Drive (trim only, no
mints).
Verified: config-equivalence gate zero unexpected diffs; extra check exit 0;
--check exit 0; validator -l 2 exit 0; and with all 30 vendor migrations
now applied, the extended validator's -f check is clean TREE-WIDE
(0 ambiguity errors, down from 1256 baseline / 16448 with the library-aware
extension); -r exit 0 for BBL and Qidi.
{input_filename_base} is meant to be the saved project's file name. Before
#13753 a bug made it fall back to the first object's name when a project was
saved; #13753 fixed it to use the project name. Some users relied on the old
behavior to get the part name into their output file name and had no
placeholder to recover it ({model_name} is the 3mf designer metadata, blank
for plain STL imports).
Add {first_object_name} as a dedicated placeholder for the first printable
object on the current plate, populated in update_object_placeholders()
independently of {input_filename_base}.
Closes#14493
Phase 2 long-tail migrations (25 collision groups), zero effective-config
change:
Snapmaker (11 + the tree's one live library-cross group): three Benchy demo
presets coexisting via compatible_prints gating (invisible to the -f check)
get their own mints; the TPE/TPU-High-Flow lines riding shared bases get
family mints across the plain/U1/Dual/J1 variants; 'Snapmaker PLA Matte @U1'
minted; 'Snapmaker PET @Dual' unified onto its own family root id;
'Snapmaker PA-CF @U1' had its compatible_printers triple-duplicated
(self-collision) - deduplicated. The PolyTerra J1/Dual PLA presets that
re-exposed the library's PolyTerra PLA (alias rename) get family mints while
'PolyTerra PLA @0.2 nozzle' keeps the library id via inheritance. Fiberon
families keep their authentic byte-copied Bambu catalog ids (sanctioned
multi-vendor brand sharing, frozen in the snapshot).
FlyingBear (8): every preset carries an own-key Bambu generic id; owners
keep theirs, 15 impostor families (S1/Ghost7/Hyper lines) re-minted
family-atomically - '@S1' and '@Ghost7' siblings move together (20 edits).
Sovol (6): the GFL99 copy-paste epidemic. The five same-alias
'Generic * @Sovol SV08 MAX' tunings just DROP their wrong own-id lines so
the library family ids flow through inheritance (alias-shadow-verified);
Generic PLA Silk / SUNLU PETG adopt their library family ids as own keys
(their inherits point at other families, so deletion would collide); the 21
dedicated Sovol/Polymaker families get fresh mints (23 edits).
'Sovol SV07 PLA' keeps GFL99 unambiguously (sole claimant on SV07), frozen
in the snapshot.
Verified: config-equivalence gate zero unexpected diffs; extra check exit 0;
--check exit 0; validator -l 2 exit 0; extended -f -v Snapmaker/FlyingBear/
Sovol all exit 0.
Phase 2 long-tail migrations (56 collision groups across six vendors), zero
effective-config change except where stated:
Cubicon (17): 'Cubicon PC @base' verbatim-copied PA-CF's P510cfd0 at
introduction (90a6c53ad5; PC was the config-light stub) - the PC family gets
a fresh mint via its root. The other groups were all one shape: nine @base
presets instantiated on exactly the two printers their dedicated variants
cover with byte-identical passthrough configs, so no compatible_printers
trim exists that CI would accept (instantiated presets must claim a
printer). Fix: flip the nine @bases to instantiation:false and drop their
setting_ids (bases carry none by convention). Every printer keeps an
identical-config selectable preset per family - verified programmatically
before flipping; no machine default_materials references an @base name.
Dremel (3): 'Dremel Generic PLA' is deliberately selectable alongside its
per-printer variants (#6837 re-exposed it), and the variants carry real
overrides - a genuine AMS ambiguity with no config-neutral trim. The three
variants share the fresh family mint OFUYjPc4 as own keys; the root keeps
GFL99. A deliberate same-family id split (P8 shape): Dremel has no device
ecosystem consuming filament_id, and every alternative is a user-visible
regression of #6837. default_materials resolve unchanged.
Anycubic (14): copy-paste/generic-riding across three id eras; 2022-era
Anycubic Generic ABS/PETG/PLA/TPU keep the catalog ids, 13 families
re-minted atomically (72 edits incl. 48 forced same-family re-ids), retiring
the invented GFABS/GFL92/GFL93/GFPLA* space-containing ids.
Artillery (8): GFL99 umbrella + P-hex copy-pastes; Artillery Generic PLA
keeps GFL99, Artillery PLA keeps Pfcf9c4c, 17 families minted (52 edits),
P284941e retired.
InfiMech (14): pure verbatim Bambu-generic copy-paste; the six InfiMech
Generic owners keep their ids, 15 impostor families minted (34 edits).
Creality (8): HF generics copy-pasted GFL99 + id-less sub-brand lines riding
keeper families; owners keep their git-verified ids, 8 families minted (17
edits, own-key layout for members inheriting heterogeneous per-series
parents).
Verified per batch: config-equivalence gate zero unexpected diffs (the nine
Cubicon @base instantiation flips are the only preset-set changes, reviewed
above); orca_extra_profile_check.py exit 0; assign_filament_ids.py --check
exit 0; validator -l 2 exit 0; extended -f exit 0 for all six vendors
(-v Cubicon/Dremel/Anycubic/Artillery/InfiMech/Creality); 14 fully-vanished
ids appended to the retirement ledger.
Phase 2 of the filament_id cleanup (31 collision groups, all one mechanism:
material-class ids on shared fdm_filament_* templates collapsing HF/-CF/
Prusament lines onto one id, plus 3 overclaims). 18 new @base family roots
(owner generic keeps the legacy id, 12 impostor families get deterministic
mints), 72 config-neutral inherits repoints, 6 template filament_id keys
removed (only where every id-less inheritor is covered by a new root;
fdm_filament_asa/pc keep theirs for the frozen Prusament @XL riders),
3 compatible_printers trims on MINIIS printers covered by dedicated @MINIIS
variants. Frozen name-shaped and _NN per-variant ids stay byte-identical.
Verified: config-equivalence gate zero unexpected diffs; extra check exit 0;
--check exit 0; validator -l 2 exit 0; extended -f -v Prusa exit 0 (was 31
groups / 1035 printer-level errors on the pre-apply copy).
Selecting the prime tower and rotating it (PageUp/PageDown) crashed.
Selection::notify_instance_update() indexed m_model->objects with the
wipe tower's synthetic id (>= 1000), which is not a ModelObject index,
so the lookup returned garbage and dereferencing it segfaulted.
do_rotate/do_scale/do_mirror already skip the wipe tower in their own
loops but all call this shared helper, so scale and mirror hit the same
fault. Selection::drop() had the same latent bug via a direct index.
Guard both with the >= 1000 check already used throughout the file.
Fixes#14498
Phase 2 of the filament_id cleanup (37 collision groups; one systematic
mistake: every commercial line inherits the material-class @base and its
E<MAT>B00 id). 23 new per-line @base roots with fresh deterministic mints
(no per-line roots existed, contrary to the plan text), 155 inherits
repoints, 23 index entries inserted between the parent @base and the line's
members (load-bearing order), 6 in-place mints on single-member generic
derivatives.
The @base-owning families keep their ids (Elegoo PLA=EPLAB00 etc.); 2 forced
family-atomic re-ids outside collision groups (Elegoo Rapid TPU 95A @EN2
Series/@Elegoo Giga follow their family). Residual 2-family shares
(GPETGB00/GASAB00 with the Centauri -CF lines, pure-template ETPUB00/
EPAHTB00) are printer-disjoint, validator-clean, and frozen in the snapshot.
Verified: config-equivalence gate zero unexpected diffs; extra check exit 0;
--check exit 0; validator -l 2 exit 0; extended -f -v Elegoo exit 0 (was 37
groups / 1332 printer-level errors on the pre-apply copy).
Phase 2 of the filament_id cleanup (69 within-vendor collision groups plus
the 9 live library-cross groups on Creator 5/5 Pro). 507 filament_id edits +
12 compatible_printers trims (G3U 0.6/0.8 bases and HS bases over-claiming
printers covered by dedicated variants):
- The FFG01 umbrella (~140 presets across many families) is broken up: the
git-verified introducing family (Flashforge Generic PLA) keeps FFG01, and
83 other families get fresh deterministic mints; the GFB99/GFG99/GFL99
G3U-era block is resolved the same way.
- Flashforge-branded presets riding OrcaFilamentLibrary generic ids
(OGFB99/OGFG99/OGFL98/OGFL99/OGFN96/OGFN98/OGFS98/OGFS99/OGFU99) get their
own family mints; true generic tunings (Generic PLA / PLA Silk / BVOH)
keep the library id with the library base name (alias-shadow-consistent).
- Layout deviation, signed off: minted ids are declared as own keys on every
instantiated member instead of new @base roots, because these families
inherit up to six heterogeneous per-era umbrella parents and
config-preserving roots would add ~80 artificial files. The snapshot
ledger records the resulting instantiated_with_id/id_overrides growth,
which is exactly the ratchet mechanism for keeping such shapes visible.
Verified: config-equivalence gate zero unexpected diffs; extra check exit 0;
--check exit 0; validator -l 2 exit 0; extended -f -v Flashforge exit 0
(was 69 groups + 9 library-cross).
Phase 1 of the filament_id cleanup (filament_id_plan.md section 5): resolve the
16 library-internal ids that covered several different materials at once and
were visible on every printer of every vendor (empty compatible_printers =
compatible with all).
- 7 existing @base roots get their copy-pasted id replaced with a fresh
deterministic mint (Elas PLA/ASA copy-pastes of Bambu-mirror ids, eSUN
copy-pastes incl. the OGFL06 eSUN PLA-Marble / Fiberon PETG-ESD polymer
mismatch).
- 30 rootless families (23 Elegoo product lines riding the material-class
E*B00 ids, Generic PETG HF / PETG-CF / PP-CF / PP-GF / PE-CF / PLA Matte
riding their parent generic's template id, PolyLite Dual PLA) each get a new
config-equivalent @base root carrying the minted id, inheriting the family's
previous parent; members re-pointed; index entries inserted before the
@System entries (loader's filament_id map is built in file order).
- Keepers by precedence stay byte-identical: Bambu-catalog mirrors
(OGFA00/OGFB01/OGFG00, PolyLite/Overture/Fiberon lines), the OFL generics
(OGFG99/OGFL99/OGFP97/OGFP99), and the Elegoo root-owning families.
Verified: config-equivalence gate — flattened effective configs of all 5892
instantiated presets are byte-identical to pre-migration except the prescribed
filament_id changes; orca_extra_profile_check.py exit 0;
assign_filament_ids.py --check exit 0; validator -l 2 exit 0 (66 vendors);
extended validator -f -v OrcaFilamentLibrary exit 0 (16 collision ids
resolved) and -f -v BBL exit 0 (all 26 BBL-shared ids are same-material
mirrors, alias-shadowed on every overlapping BBL printer — no BBL file
touched). Snapshot: +37 minted ids, id_overrides +30 (new roots override
their template parents by design).
* Support accessing `coFloatsOrPercents` values in gcode template (OrcaSlicer/OrcaSlicer#14522)
* Vector option values are separated by comma
* Fix wrong cast used for checking nullability
Phase 0 of the filament_id cleanup (filament_id_plan.md):
- scripts/assign_filament_ids.py: mint OF-prefixed 8-char ids as
uuid5(FILAMENT_ID_NAMESPACE, filament_family/<vendor>/<family>) in the
base62 derivation of the setting_id precedent; loader-faithful effective-id
resolver (vendor inherits chain + OrcaFilamentLibrary fallback); CLI:
default assign run (idempotent no-op on a fully-idded tree),
--mint Vendor/Family, --update-snapshot, --check.
- scripts/filament_id_snapshot.json: the sanctioned-state ledger (1092 ids,
1965 family claims over the current tree). The tree must equal it exactly,
both directions, so every id/claim change lands as a reviewable diff; a PR
snapshot diff is the maintainer gate. scripts/retired_filament_ids.json is
the append-only retirement ledger; --update-snapshot refuses to resurrect
retired ids and to sanction new reserved-namespace claims (GF*/QD_*/P-hex/
null) for non-owner vendors without --allow-shared-catalog.
- orca_extra_profile_check.py: runs check_filament_ids() tree-wide (format,
snapshot equality, mint conformance, retired reuse, alias hygiene for tuned
OFL generics, reserved namespaces, structure ratchet). The pre-existing
check_filament_id() 8-char rule stays BBL/OFL-scoped: grandfathered longer
ids exist elsewhere (e.g. Prusa's 36-char name ids) and are frozen via the
snapshot instead.
- PresetBundle::check_duplicate_filament_subtypes now includes
OrcaFilamentLibrary presets in every vendor's per-printer duplicate check;
alias-shadowed library presets are excluded via the existing
m_excluded_from population (verified live in the validator load path), so
only genuinely visible duplicates are flagged. AMS tray-id resolution logs
a warning when a filament_id matches 2+ compatible presets (pick unchanged).
- doc/developer-reference/filament_id.md: the authoring rule; PR template
gains a no-hand-written-ids checkbox.
- scripts/tests/test_filament_id.py: 46 stdlib-unittest tests (mint vectors,
resolver semantics, every check firing/silent, ledger round-trips, byte
preservation, real-tree smoke).
Verified: python -m unittest discover -s scripts/tests (46 OK);
python scripts/orca_extra_profile_check.py exit 0 on the unmigrated tree;
assign run is a no-op; rebuilt OrcaSlicer_profile_validator -l 2 exit 0;
extended -f is strictly additive vs baseline (every baseline group preserved,
all new groups involve library presets, alias exclusion proven by BBL-mirror
absence on BBL printers).
Windows minwindef.h defines legacy 'near'/'far' as empty macros, so MSVC
mangled `auto near = ...` (C2513) and every near(...) call (C2679/C2678 on
Vec2d). gcc/clang were unaffected, so only the Windows build failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
gcc reads "\x10cad..." as one escape (c/a/d are hex digits → 0x10CAD > 255).
Split the string literal so the \x10 escape terminates. clang let it slide;
gcc (Linux/Windows CI) errored and blocked the OrcaSlicer build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Fix network plug-in install failing when the plug-in DLL is in use (#14373)
Switching or reinstalling the Bambu network plug-in from a running
OrcaSlicer failed with "The plug-in file may be in use". install_plugin()
deleted each existing file before extracting the new one, and on Windows a
currently-loaded DLL (BambuSource.dll, or the legacy networking library)
cannot be removed or overwritten in place, so the whole install aborted.
Rename an in-use file aside to "<name>.old" before writing the new one: the
running module keeps mapping the renamed file while the new version is
extracted, so the install succeeds without having to unload the plug-in
first. Stale ".old" files are cleaned up at the start of on_init_network(),
before the plug-in is (re)loaded, so they do not accumulate.
# Description
This PR expands profile validation so we can catch backward
compatibility issues with custom presets generated by older OrcaSlicer
releases. It also adds missing `renamed_from` metadata for presets that
were renamed or moved, so older user presets can resolve their original
parent names against the current system profiles.
## Background
Many users have reported missing preset issues after upgrading past
2.4.1. Investigation showed two common causes:
- preset lookup and compatibility checks did not always account for
`renamed_from`
- some renamed base presets were missing the old preset name in their
`renamed_from` metadata
The existing profile workflow validates the current system profile tree
and a single nightly-generated custom preset bundle. That is useful for
catching current profile errors, but it does not validate user presets
generated by older OrcaSlicer versions against the current system
profiles. As a result, older missing-parent compatibility gaps can slip
through.
## Changes
- Update `check_profiles.yml` to validate historical custom preset
fixtures from `OrcaSlicer/OrcaSlicer-profile-validator`.
- Download the fixture manifest from the public `fixture-archive`
release.
- Validate each `orca_custom_presets_<version>.zip` fixture
independently against the current PR's `resources/profiles`.
- Generate per-version validation logs and upload them as workflow
artifacts.
- Fail profile validation if any historical fixture version fails.
- Add missing `renamed_from` aliases for renamed/moved presets found by
the historical fixture validation.
## Profile Compatibility Fixes
This PR adds aliases for older parent names including:
- `0.20mm Bambu Support W @BBL X1C` -> `0.20mm Standard @BBL X1C`
- `Bambu PLA Impact @BBL X1C` -> `Bambu PLA Impact @System`
- `Ginger Generic rPLA` -> `Ginger Generic PLA`
- `Ginger Generic rPETG` -> `Ginger Generic PETG`
- legacy `Panchroma PLA Stain` BBL filament names -> current `Panchroma
PLA Satin` names
- legacy Elegoo casing/name variants such as `Elegoo RAPID PLA+`,
`Elegoo RAPID PETG`, `Elegoo RAPID PETG+`, and `Elegoo PETG Pro @System`
## Validation Flow
The custom preset validation step now:
1. Downloads `manifest.json` from the `fixture-archive` release.
2. Iterates over every fixture listed in the manifest.
3. Copies the current branch's `resources/profiles` into a temporary
profile tree.
4. Removes any existing `user` directory from that temporary tree.
5. Unzips exactly one historical fixture into the temporary tree.
6. Runs `OrcaSlicer_profile_validator -p <temp profile tree> -l 2`.
7. Writes a version-specific log and a consolidated summary.
This keeps validation scoped per fixture version and avoids mixing
generated user presets from different OrcaSlicer releases.
## Fixture Source
Historical fixtures are stored as public release assets in:
`OrcaSlicer/OrcaSlicer-profile-validator`, release tag `fixture-archive`
Each release asset is expected to be named like:
```text
orca_custom_presets_v2.4.1.zip
```
## Testing
Validated locally with:
- current system profile validation
- BBL filament subtype validation
- historical custom preset fixture validation
- extra profile JSON check in a clean profile tree
The affected historical fixture set passed after adding the missing
`renamed_from` aliases.
The release manifest controls which fixture versions are validated.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
- Keyboard shortcuts (Onshape-style, three scoped layers): feature tools on
Shift+letter, 2D sketch tools on single letters (in-sketch), view/nav on
single letters (out-of-sketch). Dispatched via the DesignPanel CHAR_HOOK.
- View toggles: P = origin planes, A = world axis triad (render_view_helpers).
- Section view (non-destructive): a single horizontal clip that hides half the
model to inspect inside, showing only the solid remaining half (no ghost).
Left-panel "Section View" button or X toggles it; PageUp/PageDown move the
plane; "Flip Section" button or F shows the opposite half. Never a body/Cut.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Six fixes to the Design tab, all live-verified on :10:
- Datum-plane re-pick: wxEVT_CHOICE on m_draw_plane re-planes the live sketch
- Delete-feature dismisses its lingering settings card (on_delete_feature)
- Revert rotation-orbit regression (no feed_bodies/reload on close_tool)
- Sketch undo/delete via focus-independent CHAR_HOOK (Ctrl+Z/Y, Delete)
- Fix undo hang: reset_autoedit() clears dangling auto-edit sequence
- Titled value fields: each inline dim field shows its role (Width, Height,
Radius, Angle, Length, Side, Distance, Major, Minor)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
# Description
There is a bug that if user used to use legacy plugin, the
`NetworkAgent::use_legacy_network` will not be properly reset after user
install a newer plugin version through the plugin update dialog (ie, not
from selecting a plugin version from Preference screen). And in that
case Orca will download the legacy plugin but instead installing it with
a newer version suffix. So on next start up Orca can't find the plugin
because it tries to load using legacy version suffix instead.
This PR fixes it by getting rid of that error-prone static variable,
instead it always use the version number directly.
Fix#14373Fix#14441
# 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.
-->
<!--
> 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)
# Description
Fixes user filament presets that reference an old printer preset name in
`compatible_printers`, even when the current system printer preset
correctly declares that old name in `renamed_from`.
## Core Issue
`PresetCollection::find_preset()` was updated to resolve `renamed_from`,
but this specific failure does not go through `find_preset()`.
For root user filament presets with empty `inherits`, Orca infers
`compatible_printers` from the filament name suffix after `@`. For
example:
```json
"ELEGOO PLA Base @Elegoo Neptune 4 Pro (0.4 nozzle)"
```
loads with:
```json
"compatible_printers": ["Elegoo Neptune 4 Pro (0.4 nozzle)"]
```
The current system printer preset is named:
```text
Elegoo Neptune 4 Pro 0.4 nozzle
```
and has:
```json
"renamed_from": "Elegoo Neptune 4 Pro (0.4 nozzle)"
```
However, filament compatibility was doing a direct string comparison
against the active printer name. Since that compatibility path does not
call `find_preset()`, the `renamed_from` mapping was never considered.
## Fix
Teach printer compatibility checks to treat
`active_printer.preset.renamed_from` entries as aliases of the active
printer name.
This preserves existing user preset JSON and avoids rewriting
`compatible_printers`, while allowing old printer suffixes to remain
compatible with renamed system printer presets.
## 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)
* Add startup progress bar to splash screen
* Move progress bar down and remove percentages
* Cleaned up changes
* Update GUI_App.cpp
---------
Co-authored-by: yw4z <ywsyildiz@gmail.com>
Closes the gizmo-parity gap (bd snaporca-4h5): the five solid features that were
card-only now give in-canvas feedback like their siblings.
Draft — angle-arc drag gizmo (clone of the Revolve gizmo): once a side face is
picked, a cyan arc anchored at the face centroid shows the taper angle; drag the
tip or type the angle, the live ghost tapers with it. Axis = world +Z (the neutral
pull direction), clamped [-89, 89].
Cut — plane offset-arrow + cutting-plane rectangle (clone of the Shell arrow, adds
a wire rectangle in the cut plane sized to the target body bbox). The arrow drags
the signed offset along the plane normal; the rectangle rides at the cut position;
the ghost splits live. (Also covers bd snaporca-1gh / snaporca-mmr.)
Boolean / Sweep / Loft — operand highlighting (new by-index highlight infra):
- Boolean tints the target body teal-green and the tool body orange (per-index
body tint added to the DesignCanvas GLVolume colour loop + set_operand_bodies).
- Sweep tints the profile sketch cyan and the path sketch magenta.
- Loft tints every selected profile sketch green.
Sketch tints reuse the DisplaySketch overlay via a feature-index -> colour map
(sketch_hl_color); DisplaySketch struct unchanged. All self-gate by active tool
and clear on close_tool.
Wiring mirrors the existing gizmo pattern 1:1 (m_*_active / render_* / set_* /
clear_* / update_* in refresh_preview / DesignCanvas passthroughs / render dispatch
+ on_mouse drag branch). Both forks; DesignSketchTool.{cpp,hpp} + DesignCanvas.hpp
+ DesignPanel.hpp byte-identical across forks. Built clean on both. Draft, Cut and
Boolean highlight live-verified on :10; Sweep/Loft sketch tint is code-complete and
build-clean (visual check pending — needs hand-drawn profile/path sketches).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Sketch dimension labels — now identical to the Prepare/Preview Measure gizmo:
- draw_text repurposed to draw_dim_label: white ImGui text in a translucent-white box,
mirroring GLGizmoMeasure::render_dimensioning exactly (push_common_window_style sets the
text colour, BringWindowToDisplayFront, imgui_internal.h).
- ROOT-CAUSE FIX: world_to_screen_px multiplied two Eigen Transform3d objects
((proj * view).matrix()); a projection is not affine so Eigen mangled it -> garbage screen
coords, so labels never appeared. Now proj.matrix() * view.matrix() like Measure. (That
helper was previously [[maybe_unused]] dead code, never exercised.)
- Leaders: offset clear of the sketch line (no longer coincident with the geometry), single
point-to-point dimension line + arrows, neutral colour, width 0.6 -> 0.2.
- dim_text appends mm/in on linear dims (angles keep the degree sign).
New Design + delete:
- New "New Design" button wipes the whole document (confirm dialog) — the clear-all the
per-row Delete can't give. CadDocument::clear() now also clears bodies + display_body_meshes
(it left them stale, so solids lingered after a clear).
- on_delete_feature: a Body-row selection now shows a helpful hint (bodies are recomputed
results with no directly-removable feature) instead of silently doing nothing.
Feature tree: auto-fits its content (refresh_tree clamps height 1..9 rows, scrolls past),
instead of a fixed 140px block.
i18n (Design tab pinned English, per the UX contract):
- Restore the lost #undef _L / #define _L(s) wxString::FromUTF8(s) override atop DesignPanel.cpp;
wrap all ~54 dropdown options in _L so the single lever governs them. feature_type_name left
untranslated (machine-facing MCP JSON).
UX: per-card Value Confirm/Cancel buttons removed — the single ribbon action bar owns value
confirm/cancel via an m_value_cont guard in tool_confirm/tool_cancel. "needs a body" status
messages unified.
Both forks; DesignSketchTool.{cpp,hpp} + CadDocument.cpp byte-identical across forks. Built
clean; New Design / feature-delete / rotation / tree auto-fit live-verified on :10.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Dimensions (uniform with Prepare/Preview):
- Repurpose DesignSketchTool::draw_text -> new draw_dim_label that renders each
sketch dimension as the exact Prepare "Measure" gizmo label: white ImGui text
in a translucent-white box, positioned via the existing world_to_screen_px
projection inside the active ImGui frame. All 29 label call sites convert with
no churn; the bespoke Hershey vector font is retired.
- dim_text appends mm/in on linear dims (angles keep the degree sign).
- Placed-dimension leaders simplified to a single point-to-point line + arrows in
a neutral colour (no extension lines), matching the Measure look.
i18n (Design tab pinned English, per the UX contract):
- Restore the lost "#undef _L / #define _L(s) wxString::FromUTF8(s)" override atop
DesignPanel.cpp so one lever de-translates the whole tab, ending the half-EN/IT
state. Wrap all ~54 dropdown options in _L so the single lever governs them.
- feature_type_name left untranslated (it feeds the MCP JSON, machine-facing).
UX consistency:
- Remove the per-card Confirm/Cancel buttons from the Value card; the single ribbon
action bar now owns value confirm/cancel via an m_value_cont guard in
tool_confirm/tool_cancel (one confirm surface, per contract).
- Unify the eight divergent "needs a body" status messages to one template.
Both forks; DesignSketchTool.{cpp,hpp} byte-identical across forks. Built clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
orca_cad's test_geometry.cpp is upstream Catch2 v3 with no CAD suite, so the
surface_deviation check lives alongside the CAD tests here. Built + ran green
in orca_cad's kernel-test docker: All tests passed (4 assertions). Both forks
now carry the test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Reconstruction is no longer box-only. Five new actions, all thin
wrappers over existing CadDocument::add_* (the same kernel the GUI
calls): revolve (sketch -> add_revolve), fillet/chamfer targeted at a
measured edge id from query_topology, hole (add_hole), and boolean
(union/subtract/intersect over two bodies). extrude and revolve also
accept an optional closed profile=[[x,y],...] -> add_sketch_profile,
the Measure->Build bridge: feed a measured/sliced contour straight back.
Shared helpers plane_from / bool_from / profile_from. Each action is
transactional (checkpoint -> add -> recompute -> undo on failure ->
refresh) and returns post-state. Live-verified: fillet, chamfer (clean
edge), hole, profile-extrude (L), revolve (ring), boolean union (3->2
bodies). MCP method catalogue now 13.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Add the Build-input and Validate ends of the reverse-engineering loop.
import_step loads a STEP as native B-rep bodies (the reference part to
measure), reusing the GUI Import path (read_step_solids -> Import
features -> recompute). validate_against compares a body to a reference
({step:path} | {body:id}) and reports volume delta %, bbox delta, and
centroid offset via OCCT GProp + Bnd_Box — the RE skill's actual
acceptance metric (the "scarto %"); surface-deviation heat-map is the
upgrade path.
With this the full Understand -> Measure -> Build -> Validate loop is
live: 8 methods (describe_tools/describe_scene/query_topology/measure/
slice_body/import_step/validate_against/extrude). Verified: import a STEP
then validate body vs same STEP = 0.0%% delta / 0.0mm offset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
The "evidence" half of the reverse-engineering loop — read-only
perception that turns a body into measured numbers. query_topology
lists faces (centroid/normal, cylinder radius+axis when round) and
edges (length, circle radius); measure returns distance (and angle when
both refs have a direction) between two {face|edge|point} refs;
slice_body cross-sections a body by a base plane at an offset and
returns world polylines (sections-as-evidence). All reuse GeometryEngine
topology accessors; slice_body adds one BRepAlgoAPI_Section call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Zero-dependency Python stdio MCP server that exposes the app's control
socket as first-class MCP tools. Builds the tool list live from the
app's own describe_tools reply (introspection drives the schema), and
forwards tools/call to the Unix socket. Falls back to the slice-1 tool
set and reports a clear error when the app socket is down — never
crashes. Registered via a workspace .mcp.json (ssh stdio to the build
host); activates on the next session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Predispose the Design tab to run under an external MCP agent. New
McpControl.{cpp,hpp} embeds a line-delimited JSON-RPC 2.0 server over a
Unix domain socket, off unless env SNAPORCA_MCP is set. Requests are
marshalled onto the wx main thread and run through the SAME CadDocument
kernel the GUI uses, via two thin DesignPanel hooks (mcp_doc /
mcp_after_change) — no parallel engine.
Slice-1 methods: describe_tools (introspection -> the bridge builds tool
schemas), describe_scene (feature tree + per-body bounding boxes), and
extrude (centred rectangle sketch -> new solid). Every op returns its
full post-state. POSIX-only; Windows compiles to a no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Fusion 360 .f3d uses Autodesk's closed ShapeManager kernel — no offline
reader exists and it is input-only even in Autodesk's own cloud API, so
native import/export is infeasible. Intercept .f3d at priv::load_files
(the single funnel for drag-drop and File > Open) and show a dialog
pointing the user to export STEP from Fusion, then load any other files
in the batch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Add an "Export STEP…" button to the Design panel that writes every body to a
.step file as native B-rep (not mesh).
- CadDocument::export_step: compound all bodies (applying their per-body Move
display transform so the STEP matches what Commit ships) and write via OCCT
STEPControl_Writer (AsIs). Full error handling incl. OCCT Standard_Failure.
- DesignPanel::on_export_step: bake any open preview, wxFileDialog save, export
at the displayed body positions, status feedback.
Kernel write path verified with a standalone OCCT box->STEP->readback check
(1 solid, non-null) against the same OCCT build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Boolean features can now be re-edited from the feature tree (op, target/tool
body, keep-tool, fuzzy tolerance), funnelling through the same replace_feature
path as every other editable feature.
- on_edit_feature: route CadFeatureType::Boolean to the Boolean card.
- load_feature_into_dialog: populate the Boolean card from the saved feature.
- populate_body_choices(as_of_feature): when re-editing, list the bodies as they
existed just before the boolean (replay the recipe truncated to that slot) so a
consumed tool body still appears and the saved target/tool selections round-trip
instead of collapsing to one entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
WipeTowerIntegration::append_tcr processed filament_end_gcode with only
layer_num in its placeholder config, so a filament_end_gcode referencing
{layer_z} could not be evaluated and slicing aborted. This affects any
multi-filament print that routes tool changes through the prime/wipe tower
(for example a support filament on a Bambu printer); the same macro works
in machine_end_gcode and on the non-wipe-tower set_extruder path, which
both define layer_z.
Set layer_z to tcr.print_z, the value this function already provides to its
change_filament_gcode and tcr_rotated_gcode placeholders.
Fixes#10119
PR #13712 fixed the uninitialized Print::m_origin (commit 99dea01cc3, "Fix coord
out-of-range exception caused by m_origin memory not initialized to 0") that made
headless slice() intermittently throw ClipperLib's "Coordinate outside allowed
range". With that root cause fixed, the three tests disabled for it pass again,
so drop their [NotWorking] tags.
Port of the snaporca CAD work to the mainline fork.
- Onshape default planes (XY/XZ/YZ) at the bed centre (transparent, labelled);
modeling origin unified to the bed centre (CadDocument::modeling_origin);
world-axis triad moved to the bed centre on the Design canvas only.
- Datum plane: clickable ghost-plane base pick + draggable offset arrow.
- Slot: dims reassessed to inter-centre distance / radius / angle; fixed the
duplicate cap-arc radius quote.
- Hole: 3D cube move-handle on the face; binds to the face on the first click;
decluttered side-distance construction lines.
- Thread: derive the M spec (diameter/pitch/depth) from a picked cylindrical
surface or circular edge (GeometryEngine::circle_of_edge); fuse the helical
ridge onto the existing body; MakePipeShell fixed-binormal sweep (uniform, no
twist) + self-intersection/param guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Mirror of snaporca-cad 75045ff. Re-editing an Extrude spawned a NEW misplaced
box: build_candidate() rebuilt the feature from live tool state, but GUI
extrudes store their profile as `entities` (sketch_ref = -1) which the edit
card never restores, so the candidate had an empty profile and replace_feature
swapped in a degenerate extrude. Fix: when editing, seed the candidate from the
feature being edited and skip add-time structural re-derivation (profile source,
up-to-face, target body); the card still overrides scalar params.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Mirror of snaporca-cad a82cb0d. snaporca-88g — three [CadDocument] tests
proving recompute() replays the whole timeline, so editing any feature (not
just the last) rebuilds downstream: mid-timeline extrude edit -> fillet
rebuilds; first-feature sketch edit propagates the chain; the same survives
serialize_recipe()/deserialize_recipe(); a sketch->extrude->hole->chamfer
chain rebuilds hole+chamfer on a mid-edited extrude. All 3 pass (34 assertions,
Catch2 v3). on_edit_feature edits the tree-selected feature at any position
(13/16 types); Import/Boolean/Cut get a status message instead of a silent
no-op (full dialogs = follow-up snaporca-nu9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Mirror of snaporca-cad f3b665b. DesignCanvas had ~38 call-sites doing
`set_as_dirty(); render()` directly, tagged "llvmpipe: force repaint" —
software-GL workarounds from the headless :10 test box. On hardware GL this
bypasses the dirty/refresh cycle and burns frames outside the paint event.
Funnel all 38 through a new request_repaint() helper that branches on the
cached GL renderer string: hardware GL gets set_as_dirty()+wxGLCanvas::Refresh()
(render() runs inside the paint cycle), software GL keeps the direct render(),
undetected backend takes the safe direct path. Behaviour on the llvmpipe :10
box is byte-identical; hardware GL invalidates the canonical editor way.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Mirror of snaporca-cad 0f53a09. DesignPanel.cpp pinned every _L() to
wxString::FromUTF8 via a TU-local #define, hardcoding English and bypassing
the gettext catalog — a hard review-blocker for upstream (OrcaSlicer ships
~20 locales). Remove the override so the 463 _L("...") call-sites route
through the real Slic3r::GUI::I18N::translate (wxGetTranslation). All args
are string literals; untranslated strings fall back to the source msgid, so
English is byte-identical while other locales now translate when .po entries
exist. xgettext scans source text, so .pot extraction is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
CalibPressureAdvancePattern::line_width_first_layer() returned the raw
initial_layer_line_width, so a value of 0 (which means "use the default")
fed 0 into the Flow spacing math and threw FlowErrorNegativeSpacing,
crashing the whole app when slicing a PA pattern calibration.
Mirror the guard already present in the sibling line_width(): when the
configured width is non-positive, fall back to auto_extrusion_width.
Add a libslic3r regression test covering the width resolution.
Fixes#13188
Mirror of snaporca-cad ad822bf. The earlier mirror (2c0140afb9) implemented
persistence in the PrusaSlicer 3mf.cpp, but the GUI saves/loads projects via the
BBS-native backend (store_bbs_3mf / load_bbs_3mf), so the recipe was never
written to nor read from GUI-saved projects.
- bbs_3mf.cpp: BBS_CAD_RECIPE_FILE = "Metadata/SnapOrca_cad.bin"; writer
_add_cad_recipe_file_to_archive (called after layer-height in store_bbs_3mf);
reader branch in _load_model_from_file's metadata dispatch loop (iterate +
iequals on m_filename — mz_zip_reader_locate_file does not work on these
archives).
- Plater.cpp: load_files carries the Model-level cad_recipe onto q->model().
- test_3mf.cpp: [3mf] test asserting store_bbs_3mf embeds the recipe entry
byte-for-byte (read back via miniz, Catch2 v3).
Verified on behemoth: libslic3r_tests + the new [3mf] BBS test pass; orca-slicer
GUI links clean. Round-trip verified live on snaporca-cad (identical code path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
DesignPanel's per-body colour button references icon "color_palette" but the
asset was never ported with the CAD subsystem. On mainline OrcaSlicer the
missing bitmap throws during MainFrame construction → segfault at startup
(create_scaled_bitmap "Could not load bitmap: color_palette"). Copied from
snaporca-cad resources; runtime-loaded, no rebuild needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
Dual-fork mandate: port the parametric-recipe 3MF persistence from
snaporca-cad commits ed19eac+147e1c4 so a saved project reopens with the
editable CAD feature tree, not just the baked mesh.
Shared kernel/format/GUI (identical to snaporca-cad):
- CadDocument serialize_recipe/deserialize_recipe (cereal BinaryArchive,
versioned) + CadFeature split save/load + imported_solid<->BRep string.
- Model::cad_recipe carried through 3MF zip entry Metadata/SnapOrca_cad.bin
(writer + binary-verbatim reader branch).
- DesignPanel on_commit() stamps the recipe; on_tab_shown() rehydrates a
loaded project via load_recipe() (deserialize -> feed_bodies + refresh_tree).
Mainline-only adapters (no snaporca-cad counterpart — Catch2 v3 vs v2):
- tests/libslic3r/test_caddocument.cpp: <catch2/catch_all.hpp> +
`using Catch::Approx;` (v3 scopes Approx under Catch::).
- tests/libslic3r/CMakeLists.txt: register test_caddocument.cpp (the
original CAD port had left it out of the test build).
Verified on behemoth (snaporca-deps toolchain): libslic3r_tests clean;
[CadDocument] 17/18 (only the pre-existing tangent-to-circle SIGABRT fails,
identical to snaporca-tkz); K1 serialize round-trip + version-reject pass
(13 assertions); new [3mf] "CAD recipe blob survives a 3mf save/load cycle"
passes byte-for-byte; orca-slicer GUI links clean (186/186, DesignPanel.cpp
compiled). Interactive :10 click-through pending (no Design-tab automation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
* validator: detect duplicate filament subtype per printer (opt-in)
A filament is matched from the AMS by (filament_id + printer compatibility);
if two compatible filament presets for one printer share a filament_id, the
match is ambiguous and the runtime silently picks whichever loads first.
Add PresetBundle::check_duplicate_filament_subtypes(), gated behind a new
has_errors(check_duplicate_filament_subtypes) parameter and the validator's
-f/--check_filament_subtypes flag (off by default). For each system printer it
groups its vendor's compatible filament presets by filament_id and errors on
any group of 2+, reporting each preset as a clickable file:// URI with a single
"how to fix" hint. CI runs it for BBL only (-v BBL -f) until the other vendors'
profiles are cleaned up.
* profiles: fix ambiguous BBL filament matches
Resolve the duplicate-filament-subtype errors flagged by the validator:
- align compatible_printers with Bambu Studio where Orca over-claimed a nozzle
that already has a dedicated preset (Bambu PLA Basic/Matte/ABS @BBL H2DP;
Bambu ASA/PETG HF @BBL H2DP 0.6 nozzle; Fiberon PETG-ESD @BBL X1)
- fix a copy-pasted printer name in Overture Matte PLA @BBL A1M 0.2 nozzle
- fix a wrong inherits in Panchroma PLA Silk @BBL X1C 0.2 nozzle (was inheriting
Panchroma PLA @base, giving it filament_id GFPM001 instead of GFPM004)
Bump BBL profile version.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* profiles: enforce globally-unique, per-vendor-namespaced setting_id
Many non-Bambu vendors copied Bambu's generic setting_ids (GFSA04 alone
appeared in 1557 files), so setting_id was not globally unique. This
namespaces every vendor's ids and reserves Bambu/OrcaFilamentLibrary space.
- Reserve "G*" (Bambu) and "O*" (OrcaFilamentLibrary) id spaces.
- Assign each other vendor a 2-char prefix (first+last letter, collision
resolved) and renumber every instantiated preset to <PREFIX><NNNN>.
- Strip setting_id from base profiles (instantiation:false) per Bambu's
convention; assign one to instantiated presets that lacked it.
- Remove the pre-existing misspelled "settings_id" key (91 files).
- filament_id is left untouched (it is a per-material id).
- Add one-time migration script scripts/assign_vendor_setting_ids.py with a
persisted registry resources/profiles/vendor_prefixes.json. Re-runs freeze
existing ids; only new vendors/profiles get new ids.
- Bump version in each changed vendor index file.
- Extend scripts/orca_extra_profile_check.py with a CI guard: global
uniqueness, in-namespace, no base setting_id, no gaps, no settings_id typo.
7425 profile files changed across 61 vendors; 0 cross-vendor collisions;
validator clean; migration idempotent. BBL and OrcaFilamentLibrary id spaces
untouched.
* profiles: add setting_id authoring guide for new vendors / profiles
* profiles: drop in-repo README; setting_id guide now lives in the wiki
* profiles: derive setting_id deterministically from vendor/type/name
* bump profile version
* Resolve preset type based on nozle diameter if printer_variant is empty
* Fix incorectly resolving plastic type (PLA, ABS, etc.)
* Revert default print-variant handling as it can be not only nozzle diameter
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* CrealityPrint: use printhost_port for WebView URL instead of hardcoded :4408
Fixes#4408 — when printhost_port is configured, use that port in the
Device WebView URL instead of always defaulting to :4408.
* CrealityPrint: remove hardcoded :4408 from Device WebView URL
Drop the CrealityPrint-specific get_print_host_webui override that
unconditionally appended :4408. The generic fallback in PrintHost
already uses print_host_webui/print_host config — users who need
Mainsail on port 4408 should set print_host_webui explicitly.
Fixes#4408
Syncing the filament list from AMS with the "Overwriting" option
displayed the AMS filaments in the wrong (reversed) order in the
preview, even though the filament mapping that was actually applied
was correct. Reported on macOS with a Bambu X1C.
The preview depends on MaterialHash being iterated in material-index
order. MaterialHash is a WX_DECLARE_HASH_MAP, which under OrcaSlicer's
current wxWidgets 3.3 build (wxUSE_STD_CONTAINERS=1) resolves to
std::unordered_map; its iteration order is unspecified and on macOS
(libc++) comes out reversed, which scrambled the preview. The same
code is unaffected in BambuStudio because its older wxWidgets build
still uses wx's own key-ordered hash table, so this only became
visible after the wx upgrade.
Fixes#14335
* feat: native Windows ARM64 build support
Builds on the merged DEPS_ARCH=arm64 plumbing (#13424) by adding the
dependency and source fixes needed for a green native ARM64 build on the
windows-11-arm runner. Validated end-to-end on Snapdragon X Elite hardware
(via a downstream fork using the same fixes); see OrcaSlicer/OrcaSlicer#8271
for the full writeup.
Dependencies:
- OpenEXR 2.5.5: ImfSimd.h hard-codes IMF_HAVE_SSE2 for any MSVC, pulling in
<emmintrin.h> (x86-only) -> C1189. Patch the header to require an x86 target
and force SSE cache vars off on ARM64.
- Boost.Context: use the winfib implementation on ARM64 (Windows Fiber API)
to avoid the armasm64 / CMake ASM_ARMASM linker-module bug, while keeping
the Boost::context target Boost.Asio needs.
- OpenCV: disable WITH_IPP on ARM64 (Intel IPP/IPP-ICV is x86/x64 only;
otherwise ~200 unresolved ippicv* externals at link).
- OpenSSL: use VC-WIN64-ARM on ARM64.
- FindGLEW: add an ARM64 arch branch.
Sources:
- clipper Int128.hpp: _mul128 is an x64-only intrinsic guarded by _WIN64
(true on ARM64); guard on _M_X64 and use the portable path.
- imgui imgui_widgets.cpp: fix va_start(vaList, &text) -> va_start(vaList, text)
(the &-form compiled on x64 but is invalid on ARM64).
- crash reporter: StackWalker.cpp gains an _M_ARM64 branch; BaseException.cpp
uses Cpsr instead of the x86-only EFlags on ARM64.
CI:
- New build_windows_arm64.yml on windows-11-arm: pins CMake 3.31.x, stages
ARM64 GMP/MPFR from MSYS2 clangarm64 (with llvm-dlltool import libs),
caches deps with a fixed-depth hashFiles key, builds and uploads the binary.
OCCT/STEP, SVG-to-3D and text emboss all build and work on ARM64 (no stubs
needed). Full feature parity with x64.
* fix(ci): use forward-slash DESTDIR to avoid CMake '\a' escape error
deps configure failed at GMP/GMP.cmake: "Invalid character escape '\a'"
because DESTDIR carried Windows backslashes (C:\a\...) and is re-parsed
when re-set with the /usr/local suffix. Pass DESTDIR (and the slicer's
DEPS prefix) with forward slashes via %CD:\=/%.
* fix(ci): don't export DESTDIR env var (CMake staged-install doubles paths)
Setting a DESTDIR *environment* variable made CMake treat it as the staged
install prefix and prepend it to every dependency's install path, so e.g.
FreeType installed to <DESTDIR>/a/.../OrcaSlicer_dep/usr/local and OCCT
then couldn't find its headers. Compute the forward-slash path into a
differently-named var (ORCA_DESTDIR) and pass it only via -DDESTDIR.
* ci(windows-arm64): fold ARM64 build into the standard Windows matrix
Replace the standalone build_windows_arm64.yml with a matrix entry on the
existing build_windows job, so x64 and ARM64 share one reusable workflow
chain (build_all -> build_check_cache -> build_deps -> build_orca), per
review feedback on #14059.
- build_all.yml: build_windows now matrices over {x64: windows-latest,
arm64: windows-11-arm} and threads `arch` through. Self-hosted runner
stays x64-only.
- build_check_cache.yml: cache key and dep-prefix path are now
architecture-specific on Windows (deps/build-arm64/OrcaSlicer_dep).
- build_release_vs.bat: accept an `arm64` argument (mirrors
build_release_vs2022.bat) -> uses `-A ARM64` and the build-arm64 tree.
The top-level CMake auto-derives CMAKE_PREFIX_PATH from the build dir,
so no explicit prefix is needed.
- build_deps.yml / build_orca.yml: gate the ARM64-only prep behind
`inputs.arch == 'arm64'` -- pin CMake 3.31.x, and stage MSYS2
clangarm64 GMP/MPFR import libs. NSIS installer/PDB/profile_validator
remain x64-only; ARM64 ships the portable zip. Artifact names get an
arch suffix to avoid collisions between the two Windows jobs.
https://claude.ai/code/session_0164c7ZhCLsYBmCiVN9pWDjK
* ci(temp): generate GMP/MPFR win-arm64 blobs to commit to repo
* feat(deps): add prebuilt GMP/MPFR win-arm64 blobs
The repo ships prebuilt GMP/MPFR import libs + DLLs for win-x64 and
win-x86; the Windows ARM64 build path copies from win-${DEPS_ARCH}
(CMakeLists.txt) but the win-arm64 blobs were missing, so the slicer
configure failed at "file COPY cannot find .../win-arm64/libgmp-10.dll".
Add win-arm64 libgmp-10.{dll,lib} and libmpfr-4.{dll,lib}, generated from
the MSYS2 clangarm64 gmp/mpfr packages with MSVC-compatible import libs via
llvm-dlltool. Headers are shared across arches and unchanged.
* simplify OpenEXR.cmake
* set default arch
* support msix
* ship installer
* try to fix webview2runtime issue
---------
Co-authored-by: Adam Behrman <adam.behrman@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Adam Behrman <abehrman@users.noreply.github.com>
fix(profiles): add printable_area to fdm_U1 so all nozzle variants inherit correct 270x270 bed
The 0.2/0.6/0.8 nozzle profiles inherit from fdm_U1 which had no
printable_area defined, causing them to fall back to a smaller default
bed size. The 0.4 profile was the only one that set it explicitly.
Move printable_area and printable_height to the shared parent (fdm_U1)
so all Snapmaker U1 nozzle variants get the correct 270x270mm bed.
Bump vendor version to trigger profile re-sync on existing installs.
Co-authored-by: ni4223 <ni4223@users.noreply.github.com>
feat: forward plateindex for index-coded .gcode.3mf uploads
Gcode inside a .gcode.3mf is index-coded (Metadata/plate_<N>.gcode) and a
bundle may carry several, so the upload must name which plate to print —
even a single-plate bundle, since its entry is still indexed.
A 1-based plate index is stored in PrintHostUpload::extended_info when use_3mf
is set; the OctoPrint and Moonraker hosts forward it as a `plateindex` form
field. Servers that don't use it ignore the unknown field, so the plain G-code
path is unchanged.
Refactor skirt and brim ownership and emission flow
Refactor skirt and brim generation around a common object/group
ownership model.
Skirts and brims are now emitted as a coordinated preamble
(skirt -> brim -> object) instead of being generated and emitted
through multiple independent code paths.
Changes:
- Fix repeated skirt emission caused by the previous skirt state
tracking logic.
- Restore local skirt/brim ordering for per-object skirts in
By Layer mode.
- Emit brims together with their owning object or object group.
- Handle combined brims independently from skirt grouping.
- Handle draft shields through the same ownership model as skirts.
- Fix draft shield generation when skirt height is zero.
- Generate draft shields after brim geometry is known, preventing
draft shields from overlapping brims.
- Reject unsafe grouped per-object skirt configurations in
By Object mode.
- Remove legacy skirt emission paths and state-management
workarounds.
Support brim generation remains unchanged.
Co-authored-by: SoftFever <softfeverever@gmail.com>
The `@FF AD5M 0.25 nozzle` filament variants carried the base profile's
full printer list (AD5M/AD5M Pro/AD5X 0.4/0.6/0.8) instead of the 0.25
nozzle printers. Combined with base profiles that also listed the AD5X
0.4/0.6/0.8 printers already covered by dedicated `@FF AD5X` variants,
multiple presets with the same alias became compatible with the same
printer. The filament combobox keys presets by full name but displays
them by alias, so these surfaced as duplicate entries (e.g. "Flashforge
PLA Silk", "Flashforge ASA Basic" shown twice).
Fix the `compatible_printers` lists (data only, no settings changed):
- Repoint the 15 `@FF AD5M 0.25 nozzle` variants to the actual 0.25
nozzle printers (Adventurer 5M 0.25 + Adventurer 5M Pro 0.25).
- Remove the redundant AD5X 0.4/0.6/0.8 entries from the base profiles
where dedicated AD5X variants already exist.
- Bump Flashforge profile version to 02.04.00.02.
Each affected filament now resolves to exactly one preset per printer,
and the previously uncovered AD5M 0.25 nozzle printers gain coverage.
* Snapmaker U1: add 0.2mm and 0.8mm nozzle profiles
Add machine and process profiles for the Snapmaker U1's 0.2mm and
0.8mm nozzles, and complete the 0.6mm process lineup. Follows the
same data-only pattern used to add the 0.6 / 0.4+0.6 nozzles in
commit afc3756.
The U1 ships with 0.4, 0.4+0.6 and 0.6 nozzle options today; the 0.2
and 0.8 nozzles are supported hardware but have no profiles, so they
cannot be selected. This adds them the Orca-native way: per-nozzle
machine presets plus a model-file dropdown entry, with their process
profiles filtered in via compatible_printers.
Machine (2): lean presets inheriting fdm_U1, mirroring the existing
SM_U1_06 (0.6) preset and overriding only the per-nozzle values;
setting_ids SM_U1_02 / SM_U1_08.
Process (21): 2 per-nozzle commons (fdm_process_U1_0.2_common,
_0.8_common) holding the nozzle line widths, plus 19 profiles
(0.2: 8, 0.6: 6, 0.8: 5) that inherit their per-nozzle common and
carry their own layer height, matching upstream's factoring. The two
0.24 Standard profiles that shared id GP029 are split into
GP029_06_024 / GP029_08_024.
Model dropdown: machine/Snapmaker U1.json nozzle_diameter
"0.4;0.4+0.6;0.6" -> "0.2;0.4;0.4+0.6;0.6;0.8".
Vendor index: register the new presets in Snapmaker.json.
The existing 0.4 / 0.6 / 0.4+0.6 presets resolve identically before
and after. scripts/orca_extra_profile_check.py and the profile
validator both pass.
* chore(profiles): bump Snapmaker vendor version to 02.04.00.04
Bump the Snapmaker vendor config_version so existing installs pick up the new 0.2mm and 0.8mm U1 nozzle profiles. PresetUpdater only re-imports a vendor bundle when the shipped version is strictly greater than the cached one.
---------
Co-authored-by: ni4223 <ni4223@users.noreply.github.com>
K1C: corrige erro 'End of file' ao enviar impressao (start_print)
A K1-family fecha o WebSocket 9999 assim que aceita o comando de iniciar
impressao. O start_print fazia um ws.read() bloqueante logo apos o write, que
estourava 'End of file [asio.misc:2]' e era reportado como erro -- embora a
impressao ja tivesse iniciado (o comando e entregue no write). Torna o read e o
close best-effort (overloads com error_code), eliminando o falso erro. Mesmo
padrao ja usado em feed_filament; cobre os caminhos single-color e multicor.
wxWidgets chooses target files based on the compiler id, which makes clang-cl
look under the clang_x64_lib layout.
The dependencies are built with MSVC naming/layout, and clang-cl uses the MSVC
frontend variant on Windows. Patch the generated wxWidgets config so clang-cl
loads the vc_x64_lib targets instead.
ALL_BUILD is a Visual Studio generator target. Ninja uses `all`.
When building with -x (Ninja generator), the script fails with
"ninja: error: unknown target 'ALL_BUILD'". Use the correct target
name for each generator.
The runtime DLL copy was nested under CMAKE_CONFIGURATION_TYPES, so it only ran
for multi-config generators.
Ninja single-config leaves that variable empty, which skipped copying OCCT,
GMP, MPFR, WebView2, and freetype DLLs next to the executable. Run the copy
logic for both generator styles while keeping it Windows-only.
oneTBB enables MSVC IPO/LTCG by default, which emits MSVC proprietary bitcode
objects when built with cl.exe.
lld-link cannot consume those /GL objects. Patch the TBB MSVC compiler settings
so IPO can be disabled and the dependency produces native COFF objects that both
link.exe and lld-link can consume.
clang-cl defines MSVC in CMake, but some guarded blocks apply flags or behavior
that are specific to cl.exe and should not be passed to clang-cl.
Exclude Clang from those MSVC-only branches so clang-cl follows the compatible
compiler path instead of inheriting cl.exe-only settings.
The Windows path builds url_prefix as a wide string.
MSVC accepts concatenating the narrow literals here, but clang-cl rejects the
mixed narrow/wide expression. Use wide literals so the concatenation type matches.
clang-cl is stricter about converting the T2A_ helper result in this expression.
Cast the conversion helper result explicitly to const char* so the intended
string conversion is unambiguous across MSVC and clang-cl.
LabelItemType values are used with Marker, which is std::size_t.
MSVC accepts the implicit narrowing in this path, but clang-cl diagnoses it more
strictly. Give the enum the same underlying type as Marker.
Eigen's .cast<T>() returns a lazy CwiseUnaryOp expression, not a materialized
Matrix. The distance_to_squared overload taking a nearest_point output parameter
expects a concrete Eigen::Matrix, so template deduction fails on clang-cl.
Materialize the cast into a local Vec variable before passing it.
MSVC accepted the expression directly; clang-cl correctly rejects it.
clang-cl does not instantiate BoundingBoxBase<Point, Points>::construct for
Points::const_iterator through the same transitive path accepted by MSVC.
Add the explicit instantiation so the template definition is emitted where the
clang-cl build needs it.
Several Artillery and Flashforge machine profiles set the first-layer nozzle temperature with M104 (set, no wait) immediately before the purge/prime line. The purge then runs before the nozzle reaches temperature, so filament is extruded through a nozzle that is not yet hot enough to melt it. Changed M104 to M109 so the printer waits for the target temperature before purging.
Affected profiles:
- Artillery Sidewinder X3 Plus / X3 Pro / X4 Plus / X4 Pro (0.4 nozzle)
- Flashforge AD5X (0.25/0.4/0.6/0.8)
- Flashforge Adventurer 5M / 5M Pro (0.25/0.8 overrides + shared fdm_adventurer5m_common, which also covers the 0.4/0.6 variants via inheritance)
Refs #4337
Build the Linux AppImage for ARM64 (aarch64) alongside x86_64: the Linux CI
job now matrixes over both architectures, with arch-aware deps caching and
artifact/asset names (amd64 keeps its existing names). The aarch64 AppImage is
published to the nightly and release pages like the x86_64 one.
Run the unit-test suite on the aarch64 runner (faster GitHub arm runner); the
tests are built on that leg. Self-hosted keeps tests on the amd64 server.
* fix(libnest2d): skip the excluded-region alignment pass when there are none
NfpPlacer::finalAlign(), run from clearItems() and the destructor, always
ran the "find a best position inside the NFP of fixed items" pass even when
no items are fixed. With nothing to avoid, calcnfp() computes the inner-fit
NFP of the pile and can feed clipper a coordinate outside its allowed range.
On Linux/clang the value stays in range so it went unnoticed; on MSVC the
clipper "Coordinate outside allowed range" exception escapes the noexcept
destructor and aborts the process (exit 0xC0000409).
Build the excluded set up front and only run the pass when it is non-empty.
The block exists solely to keep the pile clear of fixed items (excluded
regions / wipe tower), so it is a no-op when there are none and the
wipe-tower behaviour is unchanged.
* test(libnest2d): remove dead nesting tests and split the suite by feature
Seven of the suite's hidden [.] test cases drove code paths Orca abandoned
at the BambuStudio fork: BottomLeftPlacer (used nowhere in src/) and the
stock default NfpPlacer backend, which returns zero bins in Orca. They have
been red since the fork and are never registered with ctest. Remove them.
Split the 1,000-line libnest2d_tests_main.cpp into per-feature files, per the
repo convention, sharing a header for the no-fit-polygon backend setup that
every translation unit must agree on (ODR):
libnest2d_tests.cpp Item and nest() basics
test_geometry.cpp geometry primitives
test_nfp.cpp no-fit-polygon machinery
libnest2d_test_utils.hpp shared includes and the NFP backend specialisation
Along the way: drop a debug exportSVG() helper that only wrote a file on test
failure (so the suite never leaves stray assets), convert the deprecated
Catch::Approx to WithinRel/WithinAbs matchers, and give the tests descriptive
names.
* test(libnest2d): add NfpPlacer unit tests
NfpPlacer is the placement engine the arranger drives, but the suite only
covered the geometry primitives. Add a fixture and five tests that exercise
pack()/accept() directly: a single item lands in the bin, an oversized item
is rejected, the first item is seeded for every starting point, many items
pack without overlap, and the rotation candidates are searched. This lifts
nfpplacer.hpp line coverage from 42% to 87% in the libnest2d suite.
* test(libslic3r): add arrangement::arrange() integration coverage
The libnest2d suite cannot reach Orca's real nesting entry point because it
does not link libslic3r. Add test_arrange.cpp driving arrangement::arrange():
items land on the bed and within bounds, do not overlap, are spaced by their
inflation, an oversized item stays unplaced, overflow spills onto virtual beds,
an empty input is a no-op, and the DONT_ALIGN and USER_DEFINED final-alignment
paths are exercised. A self-test guards the overlap check the other cases use.
* APA for overhangs - Prusa incompatibility warning
Added a sentence explicitly stating that APA for overhangs is not compatible with prusa printers
* whitespace
Clarify that "network plugin" now means *bambu* only and doesn't refer to orca cloud
fully differentiate the two offerings to avoid confusion especially for non Bambu users
* Degrees symbol don't need localization
* The Z when referring to the axis should be uppercase
* Fix the spelling of "GitHub" to camelcase
* Unify the casing of mouse button shortcuts
* Always use G-code with an hyphen
* Fix the spelling of "restricted"
* More grammar fixes
* add missing modifications
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Store user session information along with refresh token, to allow offline use once user is logged in
* Don't bother with avatar because we won't see it when offline anyway
* Fix offline Sync Presets freezing the UI on repeat clicks
Ignore restart_sync_user_preset() while a manual sync's progress dialog is on screen, so a second app-modal dialog can't stack on the first. Offline the dialog blocks on a long, uncancellable HTTP timeout; on macOS the global menu stays live while the window is disabled, so a second click otherwise wedges the app (force-quit only).
* Skip redundant user-secret re-write on startup
set_user_session() always re-encrypts and writes the secret to disk; on the startup restore path that just rewrites the bytes it was loaded from. Add a persist flag so the restore path skips it. Also drop an unused catch binding and a stray blank line.
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* feat: add support for 3MF file format in printer configurations and export options
* fix file extension
* enable 3mf for X Max 4
* disable use_3mf for X Plus 4
* Fixed an issue where `label_object_enabled` was not properly propagated to 3mf
* enable exclude object for Max 4
* remove hardcoded use 3mf for flashforge, move them to the new printer profiles config
perf(GCodeProcessor): stop recompiling std::regex on every g-code line
process_SET_VELOCITY_LIMIT() constructed three std::regex objects from
scratch on every call, and Klipper-flavor g-code contains
SET_VELOCITY_LIMIT on a large share of lines (8,834 of 103,549 lines for
a single 3DBenchy sliced for a Creality K2). perf attributes 6.4% of the
whole slicing run to this one function, almost all of it regex
compilation and the allocator traffic it generates.
process_SET_PRESSURE_ADVANCE() and the External_Purge_Tag handler had
the same per-call construction.
Hoist all five patterns to function-local static const std::regex so
they compile once. Generated g-code is byte-identical (modulo the
timestamp header); slicing a 16x Benchy plate for a K2 drops from
78.5s to 27.3s wall (2.9x) on a 16-core Linux box, single Benchy from
8.9s to 5.6s.
Co-authored-by: grant0013 <grant@harktech.co.uk>
The testing guide stated OrcaSlicer uses Catch2 v2 and advised the v2
`<catch2/catch.hpp>` include, but the vendored framework is v3.11.0
(tests/catch2/) and every test file includes `<catch2/catch_all.hpp>`.
The wrong version drove several incorrect claims: that SKIP() is
unavailable (it is, v3.3.0+), that the string matcher is "Contains"
rather than "ContainsSubstring", and that thread-safe assertions,
multiple reporters, STATIC_CHECK and built-in sharding do not exist.
Correct all version statements, the example include, and the
former "Version-Specific Limitations" section to reflect v3.11.0.
fix: apply smart preview defaults per extruder count session
- Track last extruder count (1=single, 2+=multi) instead of boolean flag
- Apply appropriate default (ColorPrint/FeatureType) when count changes
- User selections persist within same extruder count
- Symmetric behavior: both single and multi actively apply defaults
- Delete duplicate dead code block (uncommented TODO scaffolding)
Behavior:
- First slice (any type) → appropriate default
- User changes view → persists on re-slice
- Switch single→single or multi→multi → persists
- Switch single↔multi → appropriate default applies
Re-enable [OrcaCloudServiceAgent] tests now that the headless crash is fixed
The two OrcaCloudServiceAgent display-name tests were tagged [NotWorking]
in #14175 because the agent constructor dereferenced a null wxTheApp when
run headless (no wxApp is created in the unit-test binary), crashing before
any assertion ran. That null dereference was fixed in 14d2dfdd4c, which
guards wxTheApp in compute_fallback_path() and skips file persistence when
no fallback path is available.
With the fix in place both tests build and pass headless, so drop the
[NotWorking] tag and the stale explanatory comments. Verified on Linux
clang-18 (the CI compiler), headless: 20 assertions in 2 test cases pass.
Closes#14193
* Disable fff_print tests that fail only in CI
Skirt height is honored, Scenario: Skirt and brim generation, and
Scenario: PrintGCode basic functionality slice geometry that makes clipper's
coordinate range check throw "Coordinate outside allowed range" in the Linux
CI environment, while the same tests pass in local builds. Tag them
[NotWorking] so the Unit Tests job (ctest -LE NotWorking) excludes them until
the underlying slicing issue is fixed in a follow-up PR.
* Trigger Build all workflow on tests/** changes
The push and pull_request path filters did not include tests/**, so a
test-only change never started the build and the Unit Tests job never ran.
Add tests/** to both filters so changes to the test suite are built and
exercised by CI.
Fix Unit Tests CI job silently running zero tests
scripts/run_unit_tests.sh selected tests with `ctest -L "Http|PlaceholderParser"`,
but catch_discover_tests() was called without ADD_TAGS_AS_LABELS, so Catch2 tags
were never registered as CTest labels. The -L filter matched nothing and the job
passed green while running no tests ("No tests were found!!!"). Tests have not run
in CI since PR #11485 added that -L line (2025-12-23).
Register tags as labels via a shared orcaslicer_discover_tests() wrapper in
tests/CMakeLists.txt (passing ADD_TAGS_AS_LABELS), routed through all five test
suites. Restore full-suite execution by replacing the narrow -L selection with a
`-LE NotWorking` exclusion, so all reliable tests gate PRs again (the suite ran in
full before #11485).
Tag the two OrcaCloudServiceAgent display-name tests [NotWorking]: their
constructor reaches wxStandardPaths::Get().GetUserDataDir(), which dereferences
the null wxTheApp in the headless test binary and segfaults on every platform.
Excluded until the agent can be constructed without the wx app context.
CI now runs 151 tests (was 0) and passes.
* Fix null-deref and arranger bugs that gate headless slicing tests
export_gcode dereferenced a null result out-param, enum serialization
dereferenced a null keys_map, and get_arrange_polys left bed_idx unseeded so
the arranger dropped items. All only affect the headless test/CLI path.
* Fix the headless test harness and add G-code test helpers
Use the real arranger, fix temp-file handling with an RAII guard, and add
layers_with_role / max_z for inspecting sliced G-code.
* Re-enable the Model construction test
* Re-enable SupportMaterial tests and add an enforced-support test
* Re-enable and extend PrintObject layer-height and perimeter tests
* Re-enable Print skirt, brim, and solid-surface tests
* Re-enable and extend PrintGCode tests
Un-hide the basic scenario (dead-key fixes, reframes, trimmed trivia) and add
initial-layer-height, sequential-order, and null-result export tests.
* Re-enable and reframe the skirt/brim tests
Detect skirt/brim by G-code role comment instead of a sentinel speed, and
resolve the previously-unfinished skirt-enclosure test.
* Replace the stale lift()/unlift() test with a z_hop test
* Delete the stub and broken Flow tests
Fix Arch Linux dependency installation
The arch dependency script listed packages that are no longer available
in current Arch/CachyOS repositories:
- gstreamermm: removed from official repos (AUR only) and not referenced
anywhere in the OrcaSlicer build; the build uses plain gstreamer.
- webkit2gtk: replaced upstream by webkit2gtk-4.1.
Also switch the install command from `pacman -Syy` to `pacman -Syu` to
avoid the partial-upgrade pattern that Arch officially discourages on a
rolling-release distro.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add MSIX Store build design spec
* docs: update MSIX spec (PFN deep link, .drc, Associate tab) and add implementation plan
* ci: add MSIX logo asset generator and generated assets
* ci: fix MSIX asset rendering edge bleed (PixelOffsetMode) and make output order deterministic
* ci: add MSIX AppxManifest template
* ci: add MSIX packaging script
* ci: make build_msix.ps1 stage-only exit dot-source safe
* ci: build MSIX Store package in Windows job
* ci: run MSIX pack after existing Windows uploads and keep it out of release downloads
* feat: add MSIX packaged-context detection helpers
* fix: resolve MSIX package APIs dynamically to keep Win7 loadable
* feat: suppress self-update in MSIX Store build
* feat: suppress runtime file associations in MSIX Store build
* feat: keep version check in MSIX build, point update dialog at the Store
The update check is notification-only (OrcaSlicer never auto-downloads),
so the Store build keeps checking for new versions instead of skipping
the check. What changes when packaged is the new-version dialog: the
Download button is hidden, the info text asks the user to update from
the Microsoft Store, and the hyperlink / wxID_YES action opens the Store
product page instead of the GitHub release page.
* docs: align spec verification plan with Store-redirect updater behavior
* feat: default MSIX identity to the reserved Partner Center values
* feat: render MSIX logos full-bleed from the gradient-circle SVG
* feat: point update dialog Download button at the Store in MSIX builds
* feat: link Associate tab to Windows Default Apps settings in MSIX builds
* docs: align spec with review-driven logo, dialog and Associate-tab changes
* clearn up
* fix: tombstone resolution for 409 status code with error code -3
* fix: add resolution for undefined conflicts
* fix: generate setting id if it is empty for 409 tombstone
* fix: force push empty setting_id preset on 409 tombstone
* clearner solution
* Sync Elegoo profiles from ElegooSlicer
Update vendor Elegoo.json, filament/machine/process trees, and OrcaFilamentLibrary
Elegoo entries. Align machine default material names with existing filament preset names.
* feat: expose filament_name for G-code export filename format
Derive from filament_settings_id for the first active extruder and strip the suffix after @, matching ElegooSlicer so filename_format can use {filament_name}.
* chore: reorder Elegoo entries in OrcaFilamentLibrary
Group Elegoo @base profiles and bump library version to 02.03.02.62.
* sync OrcaFilamentLibrary.json with Elegoo filament profiles
* fix: clean up Elegoo process renamed_from for profile validation
Add single renamed_from only where preset names changed from legacy Orca
names; remove duplicate Rapid @System library entries that conflicted with
ECC2 vendor presets.
* fix(profiles): add missing Elegoo renamed_from for profile validation
CI custom-preset tests still inherit legacy Orca preset names that no
longer exist after the Elegoo bundle update. Add renamed_from on process,
Neptune 4 machines, OrcaFilamentLibrary filaments, and Giga profiles so
inherits resolve again, without changing print parameters.
* fix(profiles,elegoo): resolve renamed presets and CC2 SET_PRINT_STATS_INFO G-code
Resolve legacy preset names through renamed_from when validating presets and loading external projects. Add missing renamed_from aliases for Elegoo Giga process and OrcaFilamentLibrary filaments. Combine TOTAL_LAYER and CURRENT_LAYER in one SET_PRINT_STATS_INFO command on Centauri Carbon 2 (ECC2), Centauri (EC), and Centauri Carbon (ECC) 0.4 nozzle profiles.
* chore(profiles): bump Elegoo and OrcaFilamentLibrary profile versions
Refresh installed profile bundles after renamed_from aliases, CC2 SET_PRINT_STATS_INFO G-code, and Preset.cpp renamed preset resolution fixes.
* Fix junction deviation and jerk settings behavior
Process settings now follow the selected printer's junction deviation
configuration. When machine_max_junction_deviation is enabled,
default_junction_deviation is shown and jerk settings are hidden. When
junction deviation is disabled, jerk settings are restored and
default_junction_deviation is hidden.
Fix a validation issue where junction deviation mismatch warnings could
be reported even when machine_max_junction_deviation was set to 0.
Warnings now apply only when junction deviation is active and point
directly to default_junction_deviation.
Also simplify Motion ability page visibility checks by reusing local
firmware-flavor booleans.
* GUI tweak
- separate Junction Deviation segment
- JD and Jerk stay visible
Switching to a printer with fewer filaments (e.g. H2D -> X2D) threw
std::out_of_range in check_filament_printable. Clear stale per-volume
extruder config on count shrink and bound-check filament indices at the
read sites.
* Fix SeeMeCNC Multicolor change
* Fixes for Support
Support fixes for .7 and 1.0 nozzles
* Fix Retractions
* FIX
* bump version
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
## Summary
Adds end-to-end Creality K-series (K2 / K2 Plus / K2 Pro) host support
to OrcaSlicer in a single bundle, per [@SoftFever's request to
consolidate](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#issuecomment-4560837450)
the previously stacked PRs. Three logically separable features, all
gated on `host_type=crealityprint`:
1. **LAN auto-discovery** — `Browse...` in the Physical Printer dialog
now finds K-series printers on the local network via a DNS-SD
meta-browser (per-device-unique service names
`_Creality-<MAC>._udp.local.`). Other host types unchanged.
2. **CFS filament sync** — `CrealityPrintAgent` (inheriting
`MoonrakerPrinterAgent`) queries the K-series WebSocket on `:9999` for
`boxsInfo`, maps loaded CFS slots to Orca filament presets, and
populates the Sidebar via the standard `fetch_filament_info` →
`build_ams_payload` path. Matches the shape of `MoonrakerPrinterAgent` /
`QidiPrinterAgent` / `SnapmakerPrinterAgent` per [the earlier review
feedback](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#discussion_r3278574545).
3. **K-series filament profiles** — system profiles for CR-PLA / CR-PETG
/ CR-ABS / CR-Silk / CR-TPU / CR-Nylon / CR-Wood / Hyper PLA / etc. on
K2 / K2 Plus / K2 Pro nozzle sizes (imported from CrealityPrint v7.1.0+,
normalised to OrcaSlicer profile conventions).
The previous stack base (#13291, *CrealityPrint as host type*, by
@imammedo) is **also bundled into this PR** since it's currently
conflicting with main and not moving. Happy to extract it back out if
@imammedo's PR is preferred to land first for attribution — let me know.
## What this PR is *not*
- **No new UI surfaces.** All three features hook into existing UI
(Browse button, Sidebar sync icon, filament dropdowns).
- **No phone-home / telemetry.** No Hark Tech endpoints, no licence
checks, no opt-in dialogs. Pure upstream feature work.
- **No K-series-specific Device tab.** Embedded WebView falls back to
Fluidd/Mainsail on `:4408`, same shape as the existing Moonraker
integration.
## Screenshots
Captured against a K2 Combo (F021, firmware v1.1.260206) on the v4 test
build:
| | |
|---|---|
| 
| **Discovery dialog** — `Browse...` flow on a `host_type=crealityprint`
printer. Click → ~5–10 s LAN scan → K2 found with model + hostname + IP.
|
| 
| **CFS filament sync** — Sidebar after clicking the sync icon: 4 slots
populate with the real loaded CFS spools (3× Hyper PLA + 1× CR-Silk). |
| 
| **Device tab** — Mainsail loaded into the embedded WebView for
`host_type=crealityprint`, mid-print state visible. |
## What's added
### LAN discovery
- **`deps_src/mdns/`** — vendors
[mjansson/mdns](https://github.com/mjansson/mdns) (public domain) plus
Creality's `cxmdns` C++ wrapper from CrealityPrint v7.1.1 (AGPL-3.0,
compatible with OrcaSlicer's AGPL-3.0). Attribution in
`deps_src/mdns/NOTICE.md`.
- **`Utils/CrealityHostDiscovery.{hpp,cpp}`** — synchronous DNS-SD scan
+ per-host `GET /info` probe. Maps model codes `F008` / `F012` / `F021`
→ K2 Plus / K2 Pro / K2.
- **`GUI/CrealityDiscoveryDialog.{hpp,cpp}`** — modal `wxDialog` showing
Model / Hostname / IP for each discovered host.
- **`src/slic3r/CMakeLists.txt`** — adds `Iphlpapi.lib` and `Ws2_32.lib`
to `libslic3r_gui`'s MSVC link line (needed by `GetAdaptersAddresses` +
Winsock2 calls in vendored `mdns.c`).
### CFS filament sync
- **`Utils/CrealityPrintAgent.{hpp,cpp}`** — inherits
`MoonrakerPrinterAgent`, overrides `fetch_filament_info()` to query the
K-series WS protocol on `:9999`, build `AmsTrayData`, and call inherited
`build_ams_payload()`. No printer-specific code lives outside the agent.
- K2 Plus slot-state parser handles the three documented slot states
(`0` empty / `1` manually entered / `2` RFID-tagged) per [DaviBe92's
reverse-engineering docs](https://github.com/DaviBe92/k2-websocket-re).
### K-series filament profiles
- ~110 profile JSONs under `resources/profiles/Creality/filament/`
covering K2 / K2 Plus / K2 Pro × 0.2 / 0.4 / 0.6 / 0.8 nozzle combos ×
CR-PLA / CR-PETG / CR-ABS / CR-Silk / CR-TPU / CR-Nylon / CR-Wood /
Hyper PLA / Hyper PETG-GF / Hyper PLA-CF / etc.
- Imported from CrealityPrint v7.1.0; normalised to OrcaSlicer profile
conventions (tabs not spaces, no `{if !multicolor_method}` wrappers,
`filament_vendor: ["Creality"]` on Creality Generic profiles).
## Tester confirmations on the v4 test build
| Printer | Firmware | Result | Reporter |
|---|---|---|---|
| K2 Pro | v1.1.5.5 / CFS v1.4.2 | ✅ LAN discovery on #13752 test build
|
[@Requiem-MH](https://github.com/OrcaSlicer/OrcaSlicer/pull/13752#issuecomment-4495235225)
|
| K2 Pro | v1.1.5.5 / CFS v1.4.2 | ✅ CFS sync across 1-CFS, 2-CFS,
partial, full configurations |
[@Requiem-MH](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4495230061)
|
| K2 Plus | v1.1.5.2 / CFS v1.2.2 | ✅ Slot-state fix resolves the
partial-sync regression |
[@DaviBe92](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4499425852)
|
| K2 Plus | v1.1.5.5 / CFS v1.4.2 | ✅ All slots syncing correctly after
fix |
[@swilsonnc](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4503273127)
|
| K2 Plus | (Reddit u/TrainAss) | ✅ Both PLA + PETG slots populated
correctly |
[@TrainAss](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4503401664)
|
| K1C | (latest stock) | ✅ `boxsInfo` payload format compatible (4 slots
of generic PETG) |
[@JoveYu](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4519036448)
|
## Known follow-ups (out of scope)
- **Snapmaker U1 regression**
([@TrainAss](https://github.com/OrcaSlicer/OrcaSlicer/pull/13744#issuecomment-4529350262)):
the v3 build also happened to sync filament from his U1; v4 regressed
this. The refactor only touches `htCrealityPrint`-gated code so this is
likely incidental — needs his config + logs to diagnose. Will follow up
in a separate issue once this lands.
- **Native Device tab for K-series**: deferred. Current Mainsail WebView
shim covers the common case.
- **#13581 (@hamham999) profile overlap**: confirmed minimal code
conflict (zero), profile-file overlap of ~204 files. Whichever PR lands
second rebases off the other.
## Test plan
- [x] Linux build clean on commit `<UPDATED AFTER BUILD>` (LXC 104, GCC
12, cmake)
- [x] MSVC link clean (manual VS 2026 / MSVC 14.51 build)
- [x] End-to-end on real hardware: K2 Combo, K2 Pro, K2 Plus, K1C
- [x] `host_type ≠ htCrealityPrint` paths unchanged — Bonjour fires for
OctoPrint, Flashforge picker fires for Flashforge, Moonraker / Qidi /
Snapmaker agents unchanged
- [x] Profile-validation CI green (was a separate Elegoo test-fixture
failure on main, not introduced by this PR)
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Co-authored-by: Igor Mammedov <niallain@gmail.com>
Co-authored-by: grant0013 <grant@harktech.co.uk>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: hamham999 <hamham999@users.noreply.github.com>
Co-authored-by: Alys Andreollo <3528187+alysandreollo@users.noreply.github.com>
The SPARKX i7 machine presets shipped (via #13947) with a contributor's
LAN IP (http://10.10.1.39) hardcoded in print_host. Remove it to match
the K2/K2 Pro/K2 Plus presets, which carry no print_host key — LAN
discovery + the crealityprint agent populate the host at add-printer time.
This updates the SPARKX i7 from a "regular" klipper printer to use the
new CrealityPrinter agent, that talks to it's "supervisor" webserver
instead, and can use the native Creality features: CFS, filament
querying, filament remapping, etc...
* init
* update translations
* clarify warning for rebuild
* auto fill issue field on github with gathered system information
* add detection for linux package type
* fix build errors
* update
* update
* improve reading windows version
* add multi file support for zip and use timestamp on exported zip name
* fix errors
* fix errorz
* fix URL encoding
* fix CPU info
* use home or desktop as default location
* fix distro name on flatpak
* improve detecting local build on linux
* check package type on all platforms
* optimize margins
* improve monitor detection
* add support for adding text file to zip and add system info on export
* exclude user related info from config
* improve exporting profile info
* fix linux monitor info
* update
* update detecting monitor info
* Update TroubleshootDialog.cpp
* Update TroubleshootDialog.cpp
* Update TroubleshootDialog.cpp
* revert scaling and resolution detection for linux
* include project file to zip and notify after saving zip successfully saved
* improve monitor info on mac
* update
* improve packing selection menu
* update
* Update TroubleshootDialog.cpp
* Update TroubleshootDialog.cpp
* Update TroubleshootDialog.cpp
* Update TroubleshootDialog.cpp
* update
* update
* make hash clickable
* fix compatible process counting
* export profiles overview instead copying to clipboard
* auto restart app after cleaning system folder
---------
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
ws2_32 (Winsock2) and wsock32 (Winsock) are not supposed to be used in the same application.
boost::asio requires ws2_32, but wxWidgets uses wsock32.
Which gets used depends on the order they appear in the link command, as they both define the same symbols, but with different behaviour.
ws2_32 is backwards-compatible with wsock32, so wxWidgets won't be negatively affected by linking with the newer version, and prior to c228ab2da1, that's what happened.
That commit reordered how some libraries were passed to the linker, so swapped the order of these two, breaking mDNS and causing https://github.com/OrcaSlicer/OrcaSlicer/issues/13969
The CFS-aware filament sync resolves its agent via switch_printer_agent(),
which reads the preset's `printer_agent` field and falls back to "orca" when
unset -- so the K-series presets need printer_agent="crealityprint" (the id
registered by CrealityPrintAgent) in addition to host_type="crealityprint"
(classic PrintHost/LAN-discovery). Without printer_agent the Device-tab sync
defaults to the Orca agent and CFS sync doesn't engage. Set both on the
F008/F012/F021 models that supports_multi_color_print() covers (not K2 SE).
Both keys are in s_PhysicalPrinter_opts, so a new Physical Printer inherits
them from the preset.
* Add test for Arachne duplicate wall segment detection
Add test cases that reproduce an issue where Arachne generates
duplicate/coinciding extrusion segments at certain min_bead_width settings.
Test configuration:
- Profile: 0.28mm Extra Draft @BBL X1C (0.4mm nozzle, 0.28mm layer)
- outer_wall_line_width: 0.42mm, inner_wall_line_width: 0.45mm
- wall_loops: 2, precise_outer_wall: enabled
- Test polygon: outer rectangle (0,0)-(20,20) with inner cutout (0.5,0.5)-(19.5,19.5)
This creates a 0.5mm wide frame around the perimeter.
Results:
- 50% min_bead_width (0.20mm): FAILS - detects 4 duplicate segments (all 4 sides)
- 60% min_bead_width (0.24mm): PASSES - no duplicates
At 50%, Arachne generates two separate closed loops that share all 4 edges
of the inner square. At 60%, Arachne generates a single closed loop.
SVG output is exported to /tmp/opencode/ for visual debugging.
* Fix Arachne duplicate extrusion caused by bead count mismatch
WideningBeadingStrategy::compute() used optimal_width (inner wall width)
to determine if a thin wall should produce a single bead. However,
getOptimalBeadCount() uses optimal_width_outer (outer wall width) via
RedistributeBeadingStrategy to decide the bead count.
This inconsistency caused situations where getOptimalBeadCount() returned
2 beads, but compute() produced only 1 bead at full thickness. The single
bead was then generated for both inner and outer contours, resulting in
duplicate extrusion paths.
Fix: Use getTransitionThickness(1) instead of optimal_width. This method
returns the exact threshold for the 1-to-2 bead transition, ensuring
consistency between bead count calculation and bead generation.
Reproduces with: 50% min_bead_width, 0.42mm outer wall, 0.45mm inner wall,
0.5mm polygon inset creating ~0.38mm wall thickness.
Fixes#13917
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Linux compositing - retain old code and make switchable via #if statement
* Make option settable via env variable/.desktop file
* undo accidental empty row delete
Made small performance, safety, and readability improvements.
Now the logic is only called to move the build plate lower than the tallest object if the print_sequence is, "by object".
More precise checks to not move the plate beyond the maximum build volume.
OrcaSlicer currently ships an "Octo/Klipper" host type that maps to the
OctoPrint REST endpoints (api/version, api/files/local). It works for
Klipper setups that run Moonraker with the OctoPrint-emulation plugin,
but native Moonraker — and Moonraker-compatible firmwares like the
Prusa-Firmware-Buddy buddy-klipper fork — speak a different shape:
distinct paths, JSON body for /printer/print/start, {"result":...}
envelope. There's no host type for that today.
Add a new Moonraker class deriving from PrintHost. Endpoints used,
matching the Moonraker spec:
- GET /server/info — connection test, reads
result.klippy_state
- GET /server/files/roots — storage-picker dropdown
(returns roots with 'w'
permission); gracefully
degrades if absent
- POST /server/files/upload (multipart) — upload (form fields:
file, root)
- POST /printer/print/start (json) — {"filename":"<path>"}; the
filename is whatever the
upload response returned
in result.item.path, so any
server-side rename
(collision suffix etc.) is
respected. JSON body is
built via property_tree
write_json so exotic
characters in the path are
properly escaped.
Auth: X-Api-Key header, only when printhost_apikey is non-empty
(Moonraker can be configured to require it but doesn't by default).
HTTP Basic / Digest are not part of the Moonraker spec and are not
sent.
Storage root is read from upload_data.storage with "gcodes" as the
fallback default, so the existing storage-picker plumbing in
PrintHostDialogs lights up automatically once enumerable roots are
returned.
UI: registers as the "Moonraker (Klipper)" entry under host_type;
selectable via the existing Physical Printer dialog (sidebar's
connection button on the printer card).
Verified against a Prusa-Firmware-Buddy buddy-klipper fork (firmware
identifies as moonraker_version "0.8.0-prusalink-shim"): /server/info
test, multipart upload to /server/files/upload, and JSON
/printer/print/start all work end-to-end. The existing "Octo/Klipper"
entry is left untouched so users currently relying on Moonraker's
OctoPrint-emulation plugin keep working.
* feat: double-click object list row to frame object in 3D view
Resolves#13800.
Extends the existing wxEVT_DATAVIEW_ITEM_ACTIVATED handler in
ObjectList::create_objects_ctrl() so that double-clicking an
object / part / instance row calls GLCanvas3D::zoom_to_selection().
This mirrors the existing "Fit camera to scene or selected object"
canvas button, exposed via a natural mouse trigger from the list.
The current view angle is preserved (Blender-style "Frame Selected").
Scope kept intentionally small:
- Object / Part / Instance rows -> zoom_to_selection().
- Filament-color column -> unchanged (still opens color editor).
- Plate rows -> unchanged (no-op).
- Inert in slice-preview mode via get_current_canvas3D(true).
Authored with assistance from Claude (Anthropic).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: no-op object-list double-click in slice-preview mode
Following up on #13800 / #13804. The original guard used
get_current_canvas3D(true)'s `exclude_preview` flag, expecting that to
return nullptr when the preview canvas is active. In fact the flag
falls through to the editor canvas as a default, so the handler was
still calling zoom_to_selection() on the editor canvas — and since the
camera is shared between the editor and preview canvases, the move
was visible in the preview view as the camera jumping to empty world
positions (sliced or excluded, sliced or not).
Replace the misnamed flag with an explicit is_preview_shown() guard
that returns early before any canvas lookup. Manually verified:
preview mode now ignores object-list double-clicks; prepare-mode
behavior unchanged.
Authored with assistance from Claude (Anthropic).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Bump Elegoo profile version to refresh installed bundle
Bumps the Elegoo vendor profile version so installed profile bundles are refreshed from bundled resources after the recent Elegoo profile sync.\n\nThe previous version stayed at 02.04.00.00 after the profile layout changed, so existing installs could keep loading stale system/Elegoo files. That stale bundle can abort during profile loading and cause user presets inheriting from Elegoo machines, such as OrangeStorm Giga and Neptune 3 Max, to report missing parents.\n\nValidation:\n- jq parsed resources/profiles/Elegoo.json\n- verified all Elegoo.json sub_path entries exist\n- git diff --check
* Fix Elegoo process profile manifest
* Don't show unsupported presets in drop down list, since it's not useful
* Add option to show unsupported presets
* Explicitly set the default value to `false`
* update filament list without restart on preference change
---------
Co-authored-by: yw4z <ywsyildiz@gmail.com>
Since the wxWidgets 3.3 upgrade the Slice/Print split-button's transient
popup was dismissed the moment the cursor entered the gap between the
button and the menu, making "Print -> Export" impossible to select.
Anchor the menu flush against the button (with a 2 px overlap) instead of
6 px below it, removing the dead-zone the cursor had to cross.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Preserve support base outline/fill order
Honor no_sort when emitting support toolpaths to keep outline-first order.
Group tree support base paths (including lightning) into per-area no_sort collections to prevent interleaving across islands.
Keep lightning layer lookup side-effect free.
* Tag Orca specific changes
Tag Orca specific changes vs. Bambu using the comment //ORCA: . This helps when reviewing merge commits from upstream Bambu so we don't end up causing regressions when pulling in commits from upstream
* Fix: Disable redundant toolchange retraction for Elegoo Centauri Carbon
Sets `retract_length_toolchange` to 0 in the Elegoo Centauri Carbon (ECC) machine profile.
This resolves an issue where a massive filament blob would form on the prime tower immediately after resuming a manual filament change (M600). The blob was caused by a conflict between OrcaSlicer's default toolchange logic and Elegoo's hardcoded firmware behavior:
- Elegoo's firmware (specifically the `cmd_PAUSE` and `cmd_RESUME` sequences) completely takes over pressure management during an M600. It performs its own initial 2mm retraction, a 120mm purge, and a silicone brush wipe, returning the print head to the prime tower perfectly primed.
- Previously, Slicer was unaware of the firmware's priming and would issue a redundant 2mm un-retract (`G1 E2`) upon resume. Forcing 2mm of filament out of an already-full nozzle created the blob.
By disabling the toolchange retraction (`0`), Slicer correctly hands off filament pressure management during an M600 entirely to the Elegoo firmware, preventing double-retractions and eliminating the blob.
* fix errors after merging main
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Make preview slider labels draggable
Add label hit testing and delta-based dragging for the vertical preview slider labels. Keep label drags tied to the selected handle, prevent slider hover/timeline/menu handling from stealing label interactions, and keep value setters from changing the active selection implicitly.
* Refresh preview slider visuals
Update preview slider rails, handles, and labels for the refreshed light and dark theme appearance. Apply the same visual language to the horizontal slider, align single-layer and multi-layer labels, and remove obsolete triangle label geometry.
* Update the stealth mode description to reflect the current code changes in 2.4.
* disable HMS if bambu network plugin is not installed or in stealth mode
* fix build err
* add hide_login_side_panel to control whether to show login panel in home page
* fix: 409 conflicts resolution in notifications
* fix: silently log other http errors
* fix: pass force push flag to start_sync_user_preset
* remove formatting churn
* fix: propagate force push down put_setting
* refactor render_hyperlink_action to PopNotification for reuse
* fix an issue that hold status should be cleared before force pushing.
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Fix air filtration gcode emitted even if not not supported
- do not emit air filtration gcode if not supported by the printer
- removed redundant "add_eol" parameter from "set_exhaust_fan()" function
* Support 'Default' filament option (index 0)
Treat filament index 0 as the new "Default" (use active object/part filament) instead of using 1. Update config defaults and tooltips for wall/sparse/solid infill filament options (min/default -> 0, tooltip explains "Default"). Adjust normalization and propagation logic to respect explicit feature overrides and only apply base extruder when feature values are zero; only copy sparse->solid infill when sparse > 0. Introduce FeatureFilamentOverrideMask and clamp_feature_filament_to_valid to resolve and clamp feature filaments. Update UI lists and selection behavior to expose a "Default" entry and handle zero-based indices in PartPlate and Plater.
* enable_filament_for_features option
Co-Authored-By: LixNix <105106115+lixnix@users.noreply.github.com>
* \n
* Allow wipe_tower_filament to equal nozzle count
Relax the assertion in Print::extruders to permit wipe_tower_filament == config().nozzle_diameter.size(). The configuration value is 1-based and the code subtracts 1 when pushing the extruder index, so equality should be valid and selecting the last nozzle should not trigger an assertion.
* Revert "Allow wipe_tower_filament to equal nozzle count"
This reverts commit 2c976574327a8bcdc74a1b296bf1aaff7752a94e.
* Revert "enable_filament_for_features option"
This reverts commit 01c13baeddb8e26793f752deab788ee4d086975b.
* Migrate legacy feature filament defaults
Add migration logic to convert legacy feature filament selections from 1 to 0 for older 3mf files. Introduces a local migrate_legacy_feature_filament_defaults lambda in src/OrcaSlicer.cpp and src/slic3r/GUI/Plater.cpp that scans keys (wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament) on configs/objects/volumes, updates values, counts conversions and logs the result. Also adds a Semver check for "2.4.0-dev" in OrcaSlicer to trigger the migration for files older than that version. This preserves expected default filament selections when loading older project files.
* Update OrcaSlicer.cpp
* Extract migration helper to ConfigMigrations
Centralize legacy feature-filament default migration by moving the duplicated lambda into ConfigMigrations::migrate_legacy_feature_filament_defaults (src/libslic3r/Config.cpp) and declaring it in Config.hpp. Update OrcaSlicer.cpp and slic3r/GUI/Plater.cpp to call the new function instead of inline lambdas. The helper converts specific feature filament keys (wall_filament, sparse_infill_filament, solid_infill_filament, support_filament, support_interface_filament) from int 1 to 0 and returns the count of conversions to avoid duplicated migration logic.
* Remove DynamicFilamentList1Based and consolidate lists
Delete the specialized DynamicFilamentList1Based struct and its global instance. Update Choice registrations to use the single dynamic_filament_list for wall, sparse_infill and solid_infill filaments, and remove the extra update call for the removed instance. This consolidates filament choice handling and removes duplicated logic in Plater.cpp.
* move it
* fix objects
* Update Config.hpp
* Update profiles
* fix: restore version placeholder in custom G-code
PlaceholderParser sets "version" in its constructor, but Print::apply() calls clear_config() which wipes it. Unlike timestamp/user (restored during G-code export), version was never restored, so [version]/{version} threw "Variable does not exist" in custom G-code while working in output filenames.
Re-set version after both clear_config() calls so it resolves everywhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: resolve timestamp and user placeholders in File header G-code
file_start_gcode is processed via print.placeholder_parser() directly, before the G-code parser integration copy that restores timestamp/user. As a result {timestamp}, {year}..{second} and {user} threw "Variable does not exist" in the File header G-code field while working in Machine start/end G-code.
Inject fresh timestamp and user into the file_start_gcode config so they resolve, matching the other custom G-code fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: expose initial_extruder and extruded_*_total placeholders in output filenames
PrintStatistics exposed initial_tool (not its documented alias initial_extruder) and total_weight/extruded_volume (not the documented extruded_weight_total/extruded_volume_total). Filename formats using the missing names failed with "not a variable name".
Add the missing aliases to PrintStatistics::config() and placeholders().
Fixes#12436Fixes#10708
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: populate total_toolchanges without a wipe tower
total_toolchanges is documented as available while change_filament_gcode (and the wipe-tower toolchange flow) is evaluated, but it was sourced only from WipeTowerData::number_of_toolchanges, which stays -1 (clamped to 0) when no wipe tower is generated. Manual filament swaps and toolchanger/IDEX setups without a wipe tower therefore always saw total_toolchanges = 0 in custom G-code and output filenames, despite real tool changes occurring -- breaking the placeholder's documented contract.
Add a tool-ordering fallback: when number_of_toolchanges < 0, count tool changes from the print's tool ordering (the transitions in the per-layer extruder sequence). Wipe-tower prints are untouched -- number_of_toolchanges >= 0 still wins -- so their reported count does not change.
Limitation: sequential (by-object) prints without a wipe tower leave Print::tool_ordering() empty, so total_toolchanges stays 0 there (unchanged from before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ElegooLink): pass printer SN to CC2 device panel URL
The CC2 panel subscribes to MQTT topics keyed by the printer serial number.
Without sn= in the URL it uses a wrong hardcoded fallback SN, subscribes to
the wrong topics, and shows Offline permanently even though the printer is
reachable.
- Cache the SN in elegoo_cc2_test() (already fetches it, was discarding it)
- Look up cache in get_print_host_webui(); fall back to a short LAN HTTP
call on first use before the test has run
- Append sn= to the panel URL
- Clear the wrong hardcoded fallback SN/IP from the panel bundle
- Add a small synchronous boot script to the panel that fetches the SN
from the printer before the bundle reads URLSearchParams, as a fallback
for unpatched binaries
* fix(ElegooLink): persist CC2 serial number in AppConfig dev_sn section
Store the printer SN under [dev_sn] keyed by normalized print_host after
a successful connection test or system/info fetch. Reuse it on later
sessions before hitting the network, matching how access_code is keyed by
dev_id for other LAN printers.
* fix(ElegooLink): answer get_sn IPC instantly from dev_sn cache
The CC2 panel always calls get_sn with a 10s timeout. Remove the HTTP
fallback from get_sn() and resolve IPC from dev_sn/memory only so Device
tab load is not blocked after sn= is already in the URL.
* fix(ElegooLink): skip get_sn IPC when URL already has sn
The CC2 device panel calls get_sn with a 10s timeout on every MQTT
connect even when Orca passes sn= in the query string. Use the URL
serial immediately and only fall back to IPC when it is missing.
* refactor(ElegooLink): resolve CC2 SN via PrintHost::get_sn in GUI
Drop the ElegooLink.hpp include from PrinterWebViewHandler; the webview
IPC handler uses the existing PrintHost virtual instead. Keep CC2 serial
lookup helpers file-local in ElegooLink.cpp and share them between
get_sn() and get_print_host_webui().
* chore: drop redundant <memory> include in PrinterWebViewHandler
---------
Co-authored-by: SoftFever <softfeverever@gmail.com>
Wire the existing disassociate_url path into the Associate-tab
checkbox so users can revert prusaslicer/bambustudio/cura
registrations they previously enabled.
The 8dfd480c52 merge stripped <<<<<<<, =======, >>>>>>> markers
from three files. Two of the resolutions ended up syntactically
broken:
* PrintHostDialogs.hpp: both sides of the conflict added a new class
declaration in the same spot. The common closing }; lived after
the >>>>>>> marker, so concatenating without re-adding a closing
brace between the two classes left CrealityPrintHostSendDialog
unclosed. Compiler caught it via "storage class specified for
EVT_PRINTHOST_PROGRESS" at the wxDECLARE_EVENT lines (the events
were being parsed inside the still-open class body).
* PhysicalPrinterDialog.cpp: upstream's BonjourDialog fallback was
inside an `else { }` block whose closing brace remained after I
replaced the else with explicit early-return branches. Result was
one extra `}` after the Bonjour block.
No public commits affected -- the broken merge commit is still
local-only on LXC 104. Catching this before the force-push.
Consolidates #13744 (CFS filament sync) into this PR per maintainer
request and resyncs with main. Conflicts resolved:
* src/slic3r/GUI/Plater.cpp -- parallel else-if added by both sides
(htCrealityPrint + flashforge_local_api branches); kept both.
* src/slic3r/GUI/PhysicalPrinterDialog.cpp -- both sides added a
host-specific Browse dialog (Creality DNS-SD + Flashforge); kept
both as early-return branches, fall through to BonjourDialog.
* src/slic3r/GUI/PrintHostDialogs.hpp -- parallel class declarations
(CrealityPrintHostSendDialog + FlashforgePrintHostSendDialog);
kept both.
When the OrcaSlicer window is on an inactive Hyprland (or any Wayland
compositor that keeps surfaces mapped while hidden) workspace, GTK
keeps delivering synthetic leave-notify events to the printer-preset
row. The wxEVT_LEAVE_WINDOW handler at Plater.cpp:1855 calls
wxFindWindowAtPoint(), which walks the entire wxWidgets window tree
calling IsShown() / gtk_widget_get_child_visible() on each widget,
then Hide()s the edit button and triggers a Layout() of the parent
panel. The Hide()+Layout() re-fires more leave events, creating a
feedback loop that pegs a CPU core at 100% indefinitely.
GDB attached to a frozen process confirmed the main thread stuck in:
wxFindWindowAtPoint (recursing through widget tree)
-> wxWindow::IsShown
-> gtk_widget_get_child_visible
...
Sidebar::Sidebar(Plater*)::$_14 <- the leave handler lambda
wxEvtHandler::SafelyProcessEvent
wxGTKImpl::WindowLeaveCallback
gtk_main_do_event
...
IsShownOnScreen() can't be used as a guard here because GTK on Wayland
reports widgets as visible even when the toplevel surface is on an
inactive workspace (see existing comment at Plater.cpp:9304).
Fix: state-based short-circuit. If btn_edit_printer is already hidden,
the handler has no transition to perform - skip the expensive tree walk
and the Hide()+Layout() that would re-trigger the feedback loop. After
the first leave event, every subsequent leave event is O(1).
Refs:
- #12387 (open issue with matching setup: Arch + Hyprland + RTX 3060 + Bambu A1)
- #11196 (introduced the hover-edit-button feature in Nov 2025)
* Update/add re:3D profiles.
* Fix encoding issue with UTF-8 BOM
* Change spaces to tabs.
* Fix alignment-based space indentation issues.
* Test: rename_from property
* Test 2: rename_from property
* Test 3: use 'renamed' instead of 'rename'
* Add renamed property for each conflicting profile.
* Revert to optimized assets improved on [#13149](https://github.com/OrcaSlicer/OrcaSlicer/pull/13149)
# Description
Resolves https://github.com/OrcaSlicer/OrcaSlicer/issues/13830
The issue was that when OrcaSlicer was open with a 3mf file, the project
is first loaded, then when the sync finishes, it overrides the project
settings. This occurs when are working on a 3mf file and you click the
sync presets button as well.
The fix was to snapshot the current state of the settings, and then
restore whatever was marked as dirty to it's original state, preserving
the 3mf project settings.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
* Add Flashforge AD5X local send dialog, IFS mapping, and LAN discovery
* Refine Flashforge AD5X IFS dialog behavior
* Refine Flashforge IFS slot selection dialog
* Fix Flashforge printer selection and print mapping
* Use 3MF for Flashforge local uploads
* Generalize Flashforge local API handling
* Handle Flashforge local API IFS support more robustly
* Use selected plate filament info for Flashforge IFS mapping
* Fix Flashforge current-plate mapping and widget sizing
* Improve Flashforge IFS contrast and color matching
* Fix Flashforge legacy plate export and upload naming
Resolve PLATE_CURRENT_IDX before the legacy send-to-printhost path calls send_gcode so single-plate Flashforge 3MF exports target the selected plate instead of leaking the sentinel into export_3mf.
Sanitize Flashforge upload names in one shared utility reused by both the dialog and the backend client. This keeps the UI-visible filename and the actual uploaded filename consistent and replaces printer-problematic characters such as '=' without scattering Flashforge-specific logic through the generic Plater flow.
* Keep Flashforge upload filename sanitization in the backend only
Drop the PrintHostSendDialog API changes and keep filename sanitization inside the Flashforge backend paths that actually talk to the printer. This keeps the generic send dialog flow untouched while still normalizing problematic upload names for both serial and local API uploads.
* Only use the Flashforge IFS dialog for local API uploads
* Use reported Flashforge IFS support without model fallback
* Remove unused Flashforge slot uniqueness tracking
* Include <array> for Flashforge discovery message
* Sync Elegoo profiles from ElegooSlicer
Update vendor Elegoo.json, filament/machine/process trees, and OrcaFilamentLibrary
Elegoo entries. Align machine default material names with existing filament preset names.
* feat: expose filament_name for G-code export filename format
Derive from filament_settings_id for the first active extruder and strip the suffix after @, matching ElegooSlicer so filename_format can use {filament_name}.
* chore: reorder Elegoo entries in OrcaFilamentLibrary
Group Elegoo @base profiles and bump library version to 02.03.02.62.
* sync OrcaFilamentLibrary.json with Elegoo filament profiles
* fix: clean up Elegoo process renamed_from for profile validation
Add single renamed_from only where preset names changed from legacy Orca
names; remove duplicate Rapid @System library entries that conflicted with
ECC2 vendor presets.
* fix(profiles): add missing Elegoo renamed_from for profile validation
CI custom-preset tests still inherit legacy Orca preset names that no
longer exist after the Elegoo bundle update. Add renamed_from on process,
Neptune 4 machines, OrcaFilamentLibrary filaments, and Giga profiles so
inherits resolve again, without changing print parameters.
Update Snapmaker U1 (0.4 nozzle).json
Updated Snapmaker U1 0.4 nozzle "change_filament_gcode" and "machine_end_gcode" to fix the issue of collisions when using "Print by Object"
Fix compile error in Debug mode. Adds getters for Point3 types in ExtrusionEntity
ZAA changed ExtrusionPath::polyline from Polyline to Polyline3, preserving the existing interfaces by converting first_point and last_point to return a Point copy constructed from the underlying Point3 type.
ExtrusionLoop::validate function was not updated and is broken in debug configurations as it's currently comparing Point to Point3
This change promotes ExtrusionPath::first_point3/last_point3 to the ExtrusionEntity base class as a pure virtual function, implements them on derived classes, and fixes ExtrusionLoop::validate
Fix nozzle diameter guards for printers that don't report nozzle info (#13236)
PR #12814 changed DevNozzle::m_diameter default from 0.4f to 0.0f to
mean "unknown" when firmware doesn't push nozzle info, and guarded two
call sites in SelectMachine.cpp. PR #13330 introduced
DevExtderSystem::NozzleDiameterMatchesOrUnknown() and adopted it in
get_printer_preset / CalibUtils / CalibrationWizardPresetPage. A few
reachable sites were still left out and now report "mismatch" / fail
silently for every non-BBL printer (Klipper/Moonraker, RRF, Marlin,
etc.) that doesn't push BBL nozzle data.
The most visible symptom: the "Sync filament colors from AMS" button on
Moonraker printers with AMS/AFC silently does nothing, because
get_printer_preset() couldn't find a matching system preset (fixed in
#13330, but the lookup-string sites below kept the bug visible
elsewhere).
Apply NozzleDiameterMatchesOrUnknown at the two remaining comparison
sites:
src/slic3r/GUI/Plater.cpp
- file-load printer-mismatch dialog — don't prompt on every load
- on_select_preset sync_extruder_list gate — skip 0.0 extruders
For the three filament-lookup string-builder sites, fall back to the
currently-selected printer preset's nozzle diameter so the dropdown
isn't empty when firmware hasn't reported a diameter:
src/slic3r/GUI/AMSMaterialsSetting.cpp (Popup + on_select_filament)
src/slic3r/GUI/CaliHistoryDialog.cpp (get_all_filaments)
Also remove the dead SyncAmsInfoDialog::is_same_nozzle_diameters method
surfaced while auditing the affected sites — it was introduced
2024-12-30 in commit ad79ed6d93 ("ENH:add SyncAmsInfoDialog",
cherry-picked from Bambu's internal branch) but a caller was never
wired up on the OrcaSlicer side. Dead since introduction.
Fixes#13236
Refs #12814#13330
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
* Fix data race in extra bridge layer generation causing spurious bridges on top surfaces
* Guard second bridge layer against top most surfaces
* CoPilot review comments & lighting infill threading fix.
* Fix overhang preview not using fallbacks when angle is 0
The overhang visualization in Preview ("show overhangs based on support settings")
used the raw `support_threshold_angle` value from the configuration.
When `support_threshold_angle` was set to 0, Orca internally falls back to:
- 30° for tree supports
- an angle derived from `support_threshold_overlap` for normal supports
However, the preview logic ignored these fallbacks and used the raw value (0°),
leading to incorrect overhang highlighting that did not match the actual
support generation behavior.
This patch computes the effective overhang threshold used for preview:
• If `support_threshold_angle > 0`, use it directly
• If `support_threshold_angle == 0` and tree supports are used, fall back to 30°
• If `support_threshold_angle == 0` and normal supports are used, derive the
equivalent angle from `support_threshold_overlap`, `layer_height`, and the
external perimeter width.
The function now returns `normal_z` directly so the preview uses the same
effective slope threshold as the support generator.
As a result, the overhang highlight in Preview now correctly matches the
supports that will actually be generated.
* Apply Copilot suggestions
Address SoftFever review item 1 on #13752: CFS sync now lives in
CrealityPrintAgent::fetch_filament_info, matching the MoonrakerPrinterAgent
/ QidiPrinterAgent / SnapmakerPrinterAgent shape. The standard
Sidebar::sync_ams_list
-> DeviceManager::get_selected_machine()
-> Sidebar::load_ams_list(obj)
-> Sidebar::build_filament_ams_list(obj)
-> agent->fetch_filament_info(dev_id)
-> build_ams_payload(box_count, max_lane_index, trays)
path now applies to CrealityPrint hosts identically to the other
Klipper-flavoured agents. CrealityPrintAgent inherits
MoonrakerPrinterAgent, so the inherited connect_printer() ->
announce_printhost_device() -> SSDP callback already triggers
MachineObject creation in DeviceManager's localMachineList for
K-series LAN hosts; the Plater-side special case was simply bypassing
get_selected_machine() before it could fire.
Removed:
* The if (host_type == htCrealityPrint) block in
Sidebar::sync_ams_list (and the CrealityPrintAgent.hpp include it
required).
* The static CrealityPrintAgent::sync_filaments_into_ams_list()
helper that wrote directly to PresetBundle::filament_ams_list,
plus the CFSAmsListResult status struct that surfaced dialog text
back to the Sidebar.
CrealityPrintAgent::fetch_filament_info itself is unchanged - it was
already calling the inherited build_ams_payload() correctly.
Net diff: 227 deletions / 4 insertions across 3 files. No behaviour
change. Discovery + WS protocol parsing unchanged.
The fdm_filament_petg base template added in 7f46c652 (cherry-picked
from hamham999's PR 13581) had two bugs:
- filament_type: ["PLA"] (should be PETG)
- inherits: null (should chain to fdm_filament_common)
so OrcaSlicer still rejected our CR-PETG profiles even after the
base was installed.
The existing fdm_filament_pet base already declares filament_type:
["PETG"] and inherits fdm_filament_common, so it correctly serves as
the PETG base. Repoint all 10 K2-family CR-PETG profiles at it and
remove the buggy fdm_filament_petg.json + its Creality.json entry.
This effectively reverts the resource addition from 7f46c652 while
keeping the CR-PETG filament profile additions from 5612e120 (now
with a working inherits chain).
Verified locally: OrcaSlicer parses all 10 profiles without error and
the K2 family populates the Add Printer dialog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 10 CR-PETG profiles added in 5612e120 (also from hamham999s PR
13581) inherit from fdm_filament_petg, but that base template did
not exist in the Creality profile set - only fdm_filament_pet. Result:
OrcaSlicer logged
can not find inherits fdm_filament_petg for CR-PETG @Creality K2...
and aborted the rest of the Creality filament load, leaving the
filament dropdown empty for K2 users.
Adds the base template (originally added in hamham999s PR 13581) and
registers it in Creality.json filament_list after fdm_filament_pet.
Reported-by: local testing on Hark Tech 2026-05-21 test build.
Co-Authored-By: hamham999 <hamham999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from #13744 review:
1. K2 firmware payloads reference "CR-PETG" by name (per DaviBe92's
reverse-engineering work in k2-websocket-re, and a real Creality spool
seen by @swilsonnc), but the profile was missing from the Creality
filament set. Adds 10 K2-family variants:
- K2: 0.4, 0.6, 0.8 nozzles
- K2 Plus: 0.2, 0.4, 0.6, 0.8 nozzles
- K2 Pro: 0.4, 0.6, 0.8 nozzles
Profile values come from CrealityPrint v7.1.0 via @hamham999's
parallel work in OrcaSlicer/OrcaSlicer#13581. Files re-indented with
tabs and BOM stripped to match repo convention.
2. Creality HF Generic PLA and Creality HF Generic Speed PLA were missing
filament_vendor: ["Creality"] so they appeared under "General" rather
than "Creality" in the filament selector.
Reported-by: swilsonnc, DaviBe92 (CR-PETG missing)
Co-Authored-By: hamham999 <hamham999@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root-cause + fix for the K2 Plus sparse-slot / wrong-slot-mapping bug
reported by multiple testers on Reddit and PR #13744 (DaviBe92, Psych0SW,
swilsonnc, TrainAss, Gullible-Price-4257).
DaviBe92 supplied a raw /box/getRealBoxesInfo payload from a K2 Plus
running printer FW 1.1.5.2 / CFS 1.2.2 that shows the K2 Plus uses
THREE state values for slots:
state: 0 → empty
state: 1 → loaded AND currently selected as the active spool
state: 2 → loaded but not currently selected
K2 (base) and K2 Pro firmwares — confirmed against the maintainer's
test printer — only use 0/1. Our parser had assumed the 0/1 form and
filtered with `if (state != 1) continue`, dropping every state=2 slot.
Symptoms this explains:
* DaviBe92: 3 spools loaded, only 1 displayed (the state=1 slot).
* Psych0SW / swilsonnc: 2 of 4 slots returned, "2nd shows 3rd's data"
— the parser dropped slots with state=2, leaving a sparse map that
PresetBundle::sync_ams_list then packs into consecutive UI trays.
* TrainAss / Gullible-Price-4257: "No loaded slots detected" — likely
the same root cause when zero slots happen to be state=1.
Fix: treat any non-zero state as loaded. Belt-and-braces: also skip
entries that are blanked-out (vendor and type both empty) regardless of
state, in case a future firmware uses yet another encoding for empty.
No change required for K2 / K2 Pro behaviour — they already only emit
state=0 (empty) or state=1 (loaded), and the new filter accepts both.
CI flagged ~150 K2 profile files for using space indentation; repo convention
is tab indentation. Ran the upstream-provided fixer:
python3 scripts/orca_filament_lib.py -v Creality -p filament -f --force
Mechanical normalization:
- Spaces → tabs (1 tab per indent level)
- Field ordering normalized (name + type first)
- Single-value scalar fields converted to single-element arrays where the
schema expects arrays (filament_cost, filament_density,
temperature_vitrification, filament_max_volumetric_speed)
No semantic content changes. Pre-existing issue from the original K2
profile import; touching these files in the multicolor_method strip
brought them into the validator's PR-diff scope.
Per @SoftFever review on #13752, printer-specific filament sync logic
belongs in the agent rather than in Sidebar. This consolidates the
previously-duplicated code so all CFS-specific work lives in
CrealityPrintAgent.
Changes:
- New: CrealityPrintAgent::sync_filaments_into_ams_list() — static method
that builds a CrealityPrint host from a printer_cfg, queries CFS slots,
populates PresetBundle::filament_ams_list, and triggers sync_ams_list().
GUI-free; returns a result struct (Status + counts + detail) so the
caller decides what dialog to show.
- New: nested CFSAmsListResult struct describing the five possible
outcomes (Success / NotCfsCapable / QueryFailed / EmptySlots / NoMatches).
- Removed: Sidebar::sync_filaments_from_creality_cfs() entirely (its body
is now the agent method).
- Plater.hpp loses the declaration; Plater.cpp dispatches to the agent
inline within sync_ams_list() and owns only the dialog + post-sync UI
refresh (combo updates, layout, preset selection, persistence).
Two CFS-related entry points on the agent now coexist:
- fetch_filament_info() — agent-driven path; publishes via AmsTrayData
and build_ams_payload(). Active when a MachineObject is bound (BBL
concept, not currently created for Creality LAN hosts).
- sync_filaments_into_ams_list() — explicit-pull path used today by the
Sidebar's "Sync filaments" button until the K-series MachineObject
work catches up.
No user-visible behaviour change — same end-to-end flow, the data work
just lives in the agent now.
Per @SoftFever review on #13752, third-party vendored libraries belong in
deps_src/ alongside expat, imgui, hidapi, etc.
- All 5 files (mdns.{h,c}, cxmdns.{h,cpp}, NOTICE.md) move from
src/slic3r/Utils/mdns/ to deps_src/mdns/.
- deps_src/mdns/CMakeLists.txt builds mdns as a STATIC library and scopes
the MSVC Iphlpapi/Ws2_32 link requirement to that target instead of
libslic3r_gui's global MSVC block.
- deps_src/CMakeLists.txt gains add_subdirectory(mdns).
- src/slic3r/CMakeLists.txt drops the inline source listings and links
libslic3r_gui against the new mdns target; MSVC block keeps only
Setupapi.lib.
- src/slic3r/Utils/CrealityHostDiscovery.cpp #include updated to use the
include dir exposed by the new mdns target.
Verified by a clean Linux build (orca-slicer links successfully).
Two data-only fixes:
#1 Strip undefined {if !multicolor_method} wrapper from filament_start_gcode on 110 K2-Plus profiles. The placeholder is a CrealityPrint-ism that survived the profile port; Orca has no such variable, so slicing failed with a hard parser error (reported on Hyper PLA by u/Gullible-Price-4257). Inner per-layer temp logic now runs unconditionally, which is the correct default.
#4 Add filament_vendor: [Creality] to 50 Creality Generic profiles missing the field. Without it the UI grouped them under Generic instead of Creality (reported by u/mharrop94).
When a user adds a Creality K-series printer (host_type=crealityprint)
and clicks the existing "Browse" button in the Physical Printer dialog,
dispatch to a new CrealityDiscoveryDialog that finds K2 / K2 Plus /
K2 Pro printers on the LAN automatically. For other host types the
button keeps its existing BonjourDialog behaviour.
CrealityHostDiscovery (src/slic3r/Utils/CrealityHostDiscovery.{hpp,cpp})
Wraps the vendored cxmdns wrapper from the previous commit:
static std::vector<CrealityHost> scan(bool probe_info = true);
Calls cxnet::syncDiscoveryService({"Creality", "creality"}) to find
K-series printers via DNS-SD, dedupes by IP, then optionally HTTP
GETs http://<ip>/info on each match to fetch the model code
(F008 = K2 Plus, F012 = K2 Pro, F021 = K2) and MAC. Returns enriched
{ip, hostname, model_code, model_name, mac, cfs_capable} entries.
CrealityDiscoveryDialog (src/slic3r/GUI/CrealityDiscoveryDialog.{hpp,cpp})
Modal dialog with a wxListView showing Model / Hostname / IP per
discovered host. Runs CrealityHostDiscovery::scan() synchronously
with wxBusyCursor + wxWindowDisabler (5-10s total wait). User picks
one, dialog returns the IP via selected_ip().
PhysicalPrinterDialog (src/slic3r/GUI/PhysicalPrinterDialog.cpp)
The "Browse" button's click handler now reads host_type from the
edited config. If htCrealityPrint, opens CrealityDiscoveryDialog
and writes "http://<ip>" into the print_host field. Otherwise the
existing BonjourDialog path runs unchanged -- no behaviour change
for OctoPrint / Moonraker / Klipper users.
No new UI surface: one existing button now does the right thing per
host_type, mirroring how Creality Print discovers its own printers.
Drop a public-domain mDNS / DNS-SD lookup library into the tree at
src/slic3r/Utils/mdns/. Two pieces:
mdns.{h,c} -- public-domain library by Mattias Jansson from
https://github.com/mjansson/mdns
cxmdns.{h,cpp} -- C++ wrapper from CrealityOfficial/CrealityPrint
v7.1.1 (AGPL-3.0, compatible with OrcaSlicer's
AGPL-3.0)
cxmdns exposes one function:
std::vector<machine_info> cxnet::syncDiscoveryService(
const std::vector<std::string>& prefix);
It sends a DNS-SD meta-discovery query (_services._dns-sd._udp.local.),
listens ~5 seconds, and returns {ip, service_name} for every service
announcement whose name contains any of the given prefixes.
Motivation: Creality K-series firmware announces each printer under a
per-device-unique service type _Creality-<MAC-derived-hex>._udp.local.,
so OrcaSlicer's existing Bonjour code (which queries fixed service
names like _octoprint._tcp) cannot find them. The DNS-SD meta-browser
approach implemented here is the standard way to discover services
when you do not know their exact names in advance.
mdns.c calls GetAdaptersAddresses (iphlpapi) and Winsock2 functions,
neither of which were on libslic3r_gui's MSVC link line; both are
added here so the vendored sources compile and link standalone.
Attribution captured in src/slic3r/Utils/mdns/NOTICE.md. No callers
yet; the K-series discovery class + UI lands in the next commit.
The matcher tiebreaker previously preferred user-edited filament
presets over system bases on a tied score. For a K2 owner who has
a custom copy of Creality Generic PLA @K2-all called eg
Creality Hyper PLA @K2 (mine), the matcher scored both that copy
and the shipped brand-specific Hyper PLA @Creality K2 0.4 nozzle
at 30, then tiebreak picked the user copy. User copies inherit
filament_id from their parent -- in this case the generic PLA
GFL99 -- so the returned id pointed at Generic PLA, not at
Hyper PLA brand-specific id (01001). PresetBundle::sync_ams_list
then resolved by id back to Creality Generic PLA @K2-all, visibly
losing the brand on every sync.
Flip the tiebreaker to prefer system over user. The shipped
brand-specific preset always wins now and sync_ams_list lands on
the right slot label.
Drop the post-sync user-override step from the sidebar path that
was layered on to compensate -- silently substituting the user
local tuning is the wrong default for an upstream-shipped
feature; users who want their local tuning on a synced slot still
get to it via the existing combo dropdown.
Import 191 brand-specific filament presets for the K2 family of
printers (K2, K2 Plus, K2 Pro), lifted from
CrealityOfficial/CrealityPrint v7.1.1 under AGPL-3.0 (compatible
with OrcaSlicer AGPL-3.0).
Brand coverage:
CR-series: CR-PLA, CR-PLA Matte, CR-PLA Fluo, CR-Silk,
CR-TPU, CR-ABS, CR-Nylon
Hyper-series: Hyper PLA, Hyper PLA-CF, Hyper ABS, Hyper PA-CF,
Hyper PA6-CF, Hyper PAHT-CF, Hyper PA612-CF,
Hyper PC, Hyper Marble, Hyper Stardust,
Hyper L-W PLA, Hyper PPA-CF
Third-party: eSUN PLA+ / PLA-HS / PLA-Matte / PLA-Silk /
PLA-CF / PLA-LW / PLA-Lite, PolySonic PLA /
PLA Pro, Panchroma PLA Matte / Satin, Soleyin
Ultra PLA, HP Ultra PLA, HP-ASA, HP-TPU
Ender PLA variants and CrealityPrint per-K2 Generic versions of
PLA, ABS, ASA, PA, PA-CF, PAHT-CF, PET, etc.
This closes the matching gap exposed by the CFS filament-sync
work. When the K2 reports spool brand Hyper PLA or CR-PLA, the
existing Creality vendor only had a generic Creality Generic PLA
@K2-all preset to fall back to, so per-brand temps / PA / cooling
tuning was lost on every sync.
Schema and naming are drop-in compatible -- CrealityPrint
references the K2/K2 Plus/K2 Pro machine names already present in
OrcaSlicer Creality vendor (Creality K2 0.4 nozzle, etc), so no
machine-side changes or compatible_printers rewrites are required.
K2 SE filaments and the PETG/PP/PPS/HIPS family are deferred. K2
SE machine profile is not yet in OrcaSlicer Creality vendor, and
the PETG/PP/PPS/HIPS family relies on fdm_filament_* base
templates that do not exist in Orca Creality vendor -- importing
those bases from CrealityPrint did not make them register as valid
parents at preset-load time. Coverage for the missing families
will land in a follow-up once the loader requirements for new
bases are understood.
Vendor version bumped to 02.03.02.75 to trigger the per-user
profile refresh on next launch. Source attribution captured in
resources/profiles/Creality/NOTICE.md.
The agent fetch_filament_info() path does not fire for Creality
K-series hosts because Sidebar::build_filament_ams_list()
short-circuits when no MachineObject is bound. MachineObject is a
BBL cloud-connected-printer concept that does not apply to LAN
Moonraker-style hosts like the K2 -- the AMS-sync icon click was a
no-op for them.
Mirror what Creality Print own slicer does (explicit Auto Mapping
button bypassing the BBL plumbing): when the user clicks the
existing AMS-sync icon and host_type=crealityprint, dispatch to a
new Sidebar::sync_filaments_from_creality_cfs() that reads the
active printer host config, confirms the printer is a CFS-capable
K-series board, queries boxsInfo over the printer WS on port 9999,
scores each loaded slot against the user filament presets and
builds a filament_ams_list entry with the matched filament_id,
colour and slot indices, then routes through
PresetBundle::sync_ams_list so the filament combo widgets get the
same rebuild as BBL printers and runs the BBL post-sync refresh
sequence (on_filament_count_change + combo update + select_preset
+ export_selections + update_dynamic_filament_list).
No new UI surface -- the existing AMS-sync icon does the right
thing per host_type. Match-and-resolve logic is hoisted out of the
agent anonymous namespace into public statics so the sidebar can
call it without duplicating scoring rules.
K-series printers (K2, K2 Plus, K2 Pro) ship with Mainsail on port
4408. Port 80 hosts only the Creality control/upload API, which
returns 404 for unknown paths and renders as a blank/404 page in
Orca Device tab.
Override CrealityPrint::get_print_host_webui() to default to
http://<host>:4408/ when the user has not explicitly set
print_host_webui, giving K-series owners a complete printer
dashboard in the Device tab out of the box.
Score visible compatible filament presets against the CFS spool
(vendor, brand_name, type) tuple to pick the right preset:
+20 preset name contains the brand_name as a substring
(eg Hyper PLA in Hyper PLA @Creality K2 0.4 nozzle)
+10 preset name contains the vendor substring (eg Creality)
Requires preset.filament_type to equal the spool base type so a
PETG preset is never auto-picked for a PLA spool. Falls back to
filaments.filament_id_by_type(base_type) when nothing scores.
Considers both base/system presets and user-derived copies -- K2
owners frequently keep tweaked copies of system presets (per-spool
PA, temps), so filtering to bases-only would skip exactly the
presets users care about most.
Subclass of MoonrakerPrinterAgent for K-series Creality printers
(K2, K2 Plus, K2 Pro) with CFS support. Registers in
NetworkAgentFactory under host_type=crealityprint. Overrides
fetch_filament_info() to defer to base when the host is not a
CFS-capable K-series board, otherwise query the boxsInfo WebSocket
on port 9999, parse the box hierarchy into CFSSlot[], and publish
each loaded slot as an AmsTrayData entry via build_ams_payload()
so they surface in Orca filament UI.
boxsInfo schema reference (verified against K2 Combo F021 firmware
v1.1.260206):
materialBoxs[].materials[] fields: id, state, vendor, type, name,
color, pressure, rfid, percent. state=1 means loaded; box.type=0
is a CFS unit, type=1 is the external spool holder (handled by
the upload dialog).
Accepted CFS boxes are renumbered sequentially since the K2 raw
box.id has gaps for the external spool holder. Model detection is
delegated to CrealityPrint::supports_multi_color_print() added in
PR #13291.
When the spool holder is selected for printing, use the opGcodeFile
command with enableSelfTest instead of colorMatch + multiColorPrint.
This matches the protocol used by CrealityPrint desktop for spool
holder prints.
- Detect spool holder mode: any colorMatch entry with box_id 0
- Spool holder: send opGcodeFile + enableSelfTest (single command)
- CFS: send colorMatch + multiColorPrint (existing behavior)
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Show the spool holder (external filament roll) as a selectable option
in the K2 Plus filament mapping dialog. Previously only CFS slots were
shown because the box state filter excluded the spool holder.
- Only skip inactive CFS boxes (type 0, state != 1); spool holder
(type 1) is always available
- Label spool holder slots as "Ext - {type}" in the dropdown
- Auto-match priority is now:
1. CFS exact (type + color)
2. CFS type-only
3. Ext exact (type + color)
4. Ext type-only
5. Positional default
- When Ext is selected (by user or auto-match), disable other combos
since firmware does not support mixing CFS and spool holder
Signed-off-by: Igor Mammedov <niallain@gmail.com>
When the filament mapping dialog opens, automatically select the best
matching CFS slot for each gcode filament by comparing color and type.
Falls back to positional index if no exact match is found.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Query CFS material slots and show colored dropdowns to map gcode
filaments to physical printer slots. Color mapping is passed through
extended_info as colorMatch entries for the multi-color print protocol.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
---
v4:
- Use gcode tool index (T1A, T1B, ...) as colorMatch id instead of
CFS slot tool_id — firmware expects the gcode filament identifier,
not the destination slot
Add enableSelfTest checkbox to CrealityPrintHostSendDialog that
persists across sessions via AppConfig. The checkbox state is
passed to the upload via extendedInfo().
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Add model_name() to map firmware model codes (F008, F012, F021)
to human-readable names (K2 Plus, K2 Pro, K2). Update the send
dialog init() to detect multi-color support and show a group box
with the printer name.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Add CrealityPrintHostSendDialog scaffolding: an empty dialog class
that inherits from PrintHostSendDialog, and Plater wiring to use
it when the host type is CrealityPrint.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
For supported models, start_print() sends colorMatch (filament-to-slot
mapping) followed by multiColorPrint (with optional calibration via
enableSelfTest) instead of the legacy opGcodeFile command. The dialog
passes color mapping and calibration settings through extended_info.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
---
v4:
- Skip "path" form field in multipart upload for multi-color printers
(extra field breaks K2 Plus firmware parser)
- Use fire-and-forget ws.write() for colorMatch and multiColorPrint
instead of ws_send_and_read() (printer sends response asynchronously)
Add query_boxes_info() to discover loaded materials in the CFS
(Creality Filament System) via websocket. Returns the boxsInfo
JSON with slot details (type, color, vendor, temperature range).
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Add a reusable websocket helper that sends a JSON command and loops
reads until finding a response containing the expected key. This
handles the printer's unsolicited status messages that arrive on
connect before the actual response. Returns empty string on timeout
instead of throwing.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Consolidate websocket connection setup into a reusable helper.
Migrate from tcp::socket to beast::tcp_stream for timeout support.
Set SO_RCVTIMEO read timeout (3s) and connect timeout (5s).
Refactor query_boxes_info() and start_print() to use ws_connect().
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Parse the model field from the /info JSON response to enable
model-specific features. Add supports_multi_color_print() which
returns true for K2-platform printers (K2 Plus, K2 Pro, K2).
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Change start_print() from void to bool with a wxString& msg
out-parameter, matching the error handling pattern used by Duet,
MKS, ESP3D and Flashforge. On failure, the error message is now
reported to the user via error_fn instead of being silently
swallowed. Use BOOST_LOG_TRIVIAL for logging instead of std::cerr.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
Move duplicate get_host_from_url() implementations from ElegooLink and
OctoPrint into Http as a static method using the curl_url API. This
eliminates code duplication and provides a single reliable URL host
extraction utility for all print host implementations.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
get_host_from_url() returns host:port which may cause
boost::asio::ip::make_address() to fail when a port is present,
bypassing the direct IP upload path and falling through to DNS
resolution via upload_inner_with_host(). Use get_host_from_url_no_port()
to extract just the host.
Signed-off-by: Igor Mammedov <niallain@gmail.com>
2026-05-18 09:41:31 +02:00
13015 changed files with 3168594 additions and 1541888 deletions
description:Use when creating, modifying, reviewing or debugging OrcaSlicer FFF system profiles under resources/profiles, including printer/vendor/nozzle/material additions, bundle indexes and versions, preset renames, setting_id and filament_id. Also use for missing presets or vendors, ignored profile settings, ambiguous AMS filament matches, and failures from orca_profile_tool.py, check_profile.sh/.bat, OrcaSlicer_profile_validator or the Check profiles CI job.
---
# OrcaSlicer system profiles
A bundle is `resources/profiles/<Vendor>.json` plus `<Vendor>/`. The vendor id is the
filename stem, not the index's display `name`. The index is the loader's only entry point:
unindexed presets never load. `OrcaFilamentLibrary` is the shared filament bundle;
`blacklist.json` is data, not a bundle.
## Choose the reference for the task
Read the relevant reference before editing; load others only when the task crosses those areas.
Paths below are relative to this skill. Commands run from the repository root.
| Task | Read |
| --- | --- |
| Add or tune a filament, brand or material; fix compatibility / alias shadowing | [filament-profiles.md](references/filament-profiles.md) |
| Add a printer or nozzle; change models, variants, assets or extruder vectors | [machine-profiles.md](references/machine-profiles.md) |
| Add a quality tier or tune a process | [process-profiles.md](references/process-profiles.md) |
| Create a vendor bundle; diagnose loading or inheritance; migrate preset names | [vendor-bundle.md](references/vendor-bundle.md) |
| Change ids; diagnose AMS identity | [ids.md](references/ids.md), then `docs/HLSD/filament_id.md` for identity changes |
| Review a profile diff | [review-checklist.md](references/review-checklist.md) |
| Run checks, interpret failures, test another tree or verify in the app | [validation.md](references/validation.md) |
## Golden rules
1.**Bump every changed bundle's `version`**, including `OrcaFilamentLibrary.json` when affected.
Increment the last component; carry `.99` into the third component (`02.04.00.99` →
`02.04.01.00`). The updater requires a strictly newer version. CI does not check this.
2.**Register every preset, bases included, parents before children.**`update-index` generates
the four `*_list` arrays; `check` requires its output. Index names must equal file `name` fields.
3.**Generate ids; never invent or copy them.** Keep existing ids during ordinary tuning. New
presets normally omit them until `generate-id`; bases must have no `setting_id`.
BBL's authoritative `setting_id` and a wrongly inherited `filament_id` need the explicit
handling in [ids.md](references/ids.md).
4.**Load failures can discard a whole vendor bundle.** Broken `inherits`, missing indexed files,
duplicate names, invalid model/variant references and unresolved filament ids affect more than
the edited preset. Inheritance stays within a bundle, except filaments may inherit the library.
5.**Preserve shipped selectable names.** Renaming, deleting or changing `instantiation` from
`"true"` to `"false"` needs `renamed_from` on a selectable successor. It is a `;`-separated string;
update in-tree references too. See [migration rules](references/vendor-bundle.md#renamed_from).
6.**Compatibility uses exact printer variant names.** Every instantiated non-library filament
needs a non-empty `compatible_printers` in its own file. Library fallbacks may omit it;
library printer-specific tunes use a non-empty list. Keep same-product tunes disjoint.
7.**Preset values are strings or arrays of strings.** Use `"instantiation": "false"`, not `false`.
Model `nozzle_diameter` is a `;`-separated string; machine `nozzle_diameter` is an array.
Wrong types can abort loading; see [failure scopes](references/vendor-bundle.md#failure-modes-ranked-by-blast-radius).
8.**Verify setting keys against the code.** Unknown keys are silently discarded. Check
`PrintConfig.cpp` definitions and `PrintConfigDef::handle_legacy`; neighbours can contain dead
keys. `normalize` removes known obsolete keys, but does not detect arbitrary misspellings.
9.**Run the full profile checks before reporting completion.** A vendor-scoped pass is only a
development loop. Review also covers version bumps, assets, non-default processes and hardware
tuning that CI cannot establish.
## Creating or modifying a profile
1.**Inspect the diff and neighbouring presets.** Read their `name`, parent chain and children;
edits to a base or a leaf with descendants propagate. Match the bundle's structure and write
only overrides. New files use tab indentation, LF and a trailing newline; preserve unrelated
formatting in existing files. Match filename case exactly and use cross-platform names.
2.**Author explicit metadata.** Set `type` yourself, especially for `machine` vs `machine_model`.
Use `"from": "system"` and string `instantiation` on config presets. Omit ids on new presets
unless [ids.md](references/ids.md) requires special handling; retain them on existing ones.
Complete compatibility, defaults, assets and any rename migration using the task reference.
3.**Bump the version**, then run the authoring commands in order for each affected bundle:
| A setting has no effect | Key spelling/type, `handle_legacy`, or a config key placed on a `machine_model` |
| A preset exists but is not selectable | Index registration, `instantiation`, installation and compatibility |
| A filament is missing, duplicated, or matches the wrong spool | [Compatibility and alias shadowing](references/filament-profiles.md#compatible_printers); [ids](references/ids.md) |
| A bed temperature is ignored | [Plate-specific temperature keys](references/filament-profiles.md#bed-temperature-is-twelve-keys-not-one) |
| A change is absent from the running app | Version bump and [installed profile location](references/validation.md#testing-in-the-app) |
| A check fails | [Error → remedy](references/validation.md#error--remedy) |
## Source of truth
When guidance and behavior disagree, inspect the current checkout:
`scripts/orca_profile_tool.py` for tooling and flags; `src/libslic3r/Preset*.cpp` for loading and
compatibility; `src/libslic3r/PrintConfig.cpp` for setting types and legacy handling;
`src/dev-utils/OrcaSlicer_profile_validator.cpp` and `.github/workflows/check_profiles.yml` for
validation coverage. `docs/HLSD/filament_id.md` defines filament identity. The
[profile development guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/how_to_create_profiles.md)
is a tutorial; confirm loader and CLI details against these sources.
| Generic material for all printers | `OrcaFilamentLibrary/filament/Generic <mat> @System.json` |
| A brand's product, all printers | `OrcaFilamentLibrary/filament/<Brand>/` |
| A brand's tune for one printer | `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/` — recommended; `<PrinterVendor>/filament/<Brand>/` also works |
| A printer vendor's tune of a generic or its own product | `<Vendor>/filament/` |
Both locations for the last-but-one row are supported: `OrcaFilamentLibrary/filament/<Brand>/<PrinterVendor>/<Name>.json`
(the shape the wiki shows) and `<PrinterVendor>/filament/<Brand>/`. The library path is the one a
filament vendor should contribute to — `OrcaFilamentLibrary/filament/<Brand>/` is the brand's own
folder, while a printer vendor's folder belongs to that printer vendor. Brand tunes do ship under
printer vendors' folders today (Polymaker and SUNLU among others).
Library layout: `filament/base/fdm_filament_*.json` type roots, root-level `Generic <mat> @System.json`
generics, and one subfolder per brand, which may nest printer-specific tunes one level deeper. Adding
a brand means adding a folder here; the folder name is a directory label only — `filament_vendor` inside the JSON is the real vendor string.
## The three-part shape
```jsonc
// Fiberon PA6-CF @base.json — the product root, holds identity + material values
| identity | **the `name` of the `machine_model_list` entry**, which is what a variant's `printer_model` must equal. `check_name_consistency` forces it to equal the file's `name`, so they coincide. |
| `model_id` | a *separate* cloud/device printer type. Optional, and not required to be unique. Not the model's identity. Changing it changes device matching. |
| `machine_tech` | only `starts_with("SL")` means SLA; everything else is FFF. Write `FFF`; a few models write `FGF`, which is a label with no effect. |
| `nozzle_diameter` | `;`-separated string, one token per available size. Order is free (Qidi writes `0.4;0.2;0.6;0.8` to put the default first). This list is the authoritative set of legal `printer_variant` values. |
| `default_materials` | `;`-separated filament **preset names**. Used to preselect in the wizard *and* by `PresetBundle::load_installed_filaments` to auto-install a printer's filaments on first run, so a dangling entry costs a real user a filament. Not `,`; case-sensitive (`@System`). `check` fails on a dangling name here or in `default_filament_profile`. |
| `family` | a wizard grouping label only; give every model one. |
### Assets
`bed_model`, `bed_texture` and `hotend_model` are paths relative to the **vendor folder** (by id).
Majority convention: `<Model>_buildplate_model.stl` and `<Model>_buildplate_texture.svg`. An empty string
is the legal "none", and is the norm for `hotend_model`.
**Nothing checks that the file exists.** A missing `hotend_model` falls back to
`resources/profiles/hotend.stl`; a missing `bed_model`/`bed_texture` just renders nothing. Broken
references already ship. Verify by hand.
Every model also has a `<Model>_cover.png` in the vendor folder — treat it as required, not optional.
240×240 is the cap `scripts/optimize_cover_images.py` enforces and the size most covers already use.
A missing cover degrades to a placeholder in both the wizard and the sidebar.
## The `machine` variant
```json
{
"type":"machine",
"name":"Phrozen Arco 0.4 nozzle",
"inherits":"fdm_machine_common",
"from":"system",
"setting_id":"lvaYKTUZr5C9jSwk",
"instantiation":"true",
"printer_model":"Phrozen Arco",
"printer_variant":"0.4",
"nozzle_diameter":["0.4"],
"default_print_profile":"0.20mm Standard @Phrozen Arco 0.4 nozzle",
Start with delivery, identity and backward compatibility, then check the affected preset types.
The table highlights gaps that need human review. What CI *does* run:
[validation.md](validation.md).
| Not checked by CI | Consequence |
| --- | --- |
| The `version` bump | The change never reaches an upgrading user |
| A misspelled setting key | Setting silently has no effect |
| A filename Windows cannot check out, or one that differs from its `sub_path` only in case | Works on the author's machine, breaks the bundle on another platform |
| `bed_model` / `bed_texture` / `hotend_model` pointing at a missing asset | Bed renders as Custom, hotend falls back to the generic model |
| A nozzle size in a model's list with no matching variant | The size is offered and resolves to nothing |
| A non-default process | `validate_slice` gives non-default quality tiers no dedicated coverage |
| Whether the intended default survived compatibility selection | The sweep can select a different compatible preset |
| A dangling `compatible_printers` inside an `instantiation: "false"` base | A base never becomes a `Preset`, so the reference check never sees it (a bad `inherits` in a base *is* caught) |
| A `renamed_from` whose old name is still a live preset | The redirect is inert while a live preset carries that name |
| Per-extruder vector length on a multi-nozzle printer | Silently padded (with the **first** value) or truncated |
## 1. Was the vendor `version` bumped?
For **every** bundle whose folder the diff touches, `resources/profiles/<Vendor>.json` must have its
`version` incremented — last component, carrying `.99` into the third component. A library change
means bumping `OrcaFilamentLibrary.json`.
*Why:* nothing in CI checks it, and `PresetUpdater` reinstalls only when `vendor_ver < resource_ver` —
without a bump the change reaches neither an upgrading user nor the author's own running app.
## 2. Was the index rebuilt, and does the diff contain only this change?
`check` now fails on an unregistered file, on an index `update-index` would reorder, and on a file
`normalize` would rewrite — so a PR that skipped them arrives red, and you do not have to spot the
omission yourself. Three things are still yours:
- **The index diff belongs to this change.** `update-index` rewrites whole `*_list` sections. If the
bundle had drifted, the author's PR now carries someone else's reordering; ask for it in a separate
commit rather than reviewing it inline.
- **A deleted selectable preset needs a successor** as in item 4. `update-index` removes its
registration; `validate_custom` detects the break only for names covered by released fixtures.
- **`normalize` edits content, not just layout.** It drops `version` and `is_custom_defined` from preset
files, removes obsolete keys, deletes six print-speed keys from filament profiles, and resolves
`extruder_clearance_radius` against `extruder_clearance_max_radius` by keeping the larger.
Check that the keys it removed were meant to go.
Obsolete keys fail `check`'s normalization pass and should be removed with `normalize`.
`check` also reports per-key obsolete warnings for filament profiles in the selected vendors.
*Why:* the index is the loader's only entry point. Out-of-order entries fail with `can not find inherits`
and take the whole vendor bundle down; an unindexed file gets reviewed, merged and never loads.
## 3. Are ids generated, not written?
No hand-typed or copied `setting_id` / `filament_id`. Instantiated presets have a `setting_id`; bases do
not. `check` enforces all of that; what it cannot tell you is whether the identity *should* have moved.
A rewritten or removed `filament_id` means a product's identity moved — a rename, or an edited
`filament_vendor` / `filament_type` — and the old id is not forwarded anywhere. Confirm that was
intended, and that a new id is not a rename in disguise.
*Why:* a duplicate `filament_id` on one printer makes AMS spool matching a coin toss; a copied
`setting_id` breaks preset identity. See [ids.md](ids.md).
## 4. Does anything disappear for existing users?
A rename, a deletion, or a flip of `"instantiation": "true"` → `"false"` on a shipped preset removes the
name from the preset collection. It needs `renamed_from` on a successor — and only one preset may claim a
given old name. The claimed old name must **not** still be a live preset; the redirect is inert if it is.
*Why:* user presets inheriting it die with `can not find parent <name> for config <file>!`; 3MF-embedded
presets are dropped with no error at all. Commit `33923464ae` reverted exactly this for Cubicon;
`6943b6ddc3` redid it correctly. CI's `validate_custom` catches the shipped-name case — but not an inert
`renamed_from`.
## 5. Is `compatible_printers` right?
Exact printer **variant** names, non-empty on every instantiated filament outside OrcaFilamentLibrary
and written in the preset's own file — golden rule 6, with the flattened-vs-own-key trap in
[filament-profiles.md](filament-profiles.md#compatible_printers). Watch for a nozzle-specific variant that
inherited or copied the base's full printer list, and for two presets of one product with overlapping
lists — duplicate combobox entries and an ambiguous AMS match.
*Why:* real shipped bugs twice (`b7b3418baf` "showing up everywhere", `ff83aa41ef` duplicate Flashforge
entries).
## 6. Model ↔ variant ↔ process consistency
- New nozzle size → the model's `nozzle_diameter` list extended, a variant with a matching
`printer_variant`, and at least one process listing that variant.
-`default_print_profile` is one exact name (not a `;` list), and that process's resolved
compatibility list or condition includes this printer.
-`default_filament_profile` is an array of names that exist.
*Why:* an unlisted `printer_variant` is a hard bundle-load failure. Default process selection is
weaker: the sweep attempts the named default, then updates compatibility and rejects generic Default
fallbacks. Another compatible process can conceal a bad reference, so inspect it even after a pass.
## 7. Types and spellings
Every value a string or an array of strings; `filament_type` an array; `instantiation` the string
`"true"`/`"false"` — golden rule 7. Check index metadata and model `nozzle_diameter` especially;
wrong types there can abort loading for **every** vendor.
The part only a reviewer can do: check new setting keys against `src/libslic3r/PrintConfig.cpp`. A
misspelled key is silently discarded (rule 8), the single most common way a profile edit does nothing
while CI stays green.
## 8. Blast radius of a base edit
A change to `fdm_*_common.json` reaches every child at once. Ask which presets it touches — several
reverts in this repo are exactly this (`41d1b0d3c8`, `dc491166a8`). Also check whether the edited leaf has
children of its own: Prusa, Flashforge and Elegoo all chain leaf-inherits-leaf several levels deep.
## 9. Do the numbers make sense for the nozzle?
Check resolved widths and layer heights against the nozzle, and flow limits / pressure advance
against the actual hardware and material. The patterns in [process-profiles.md](process-profiles.md)
are examples, not mandatory values; [filament-profiles.md](filament-profiles.md) explains what to
revisit for a nozzle change. A cloned preset's unchanged MVS needs particular scrutiny.
Settings tuned for real hardware cannot be verified by reading the diff. Say so rather than approving
numbers nobody measured.
## 10. Asset references (not checked anywhere)
`bed_model`, `bed_texture`, `hotend_model` and `<Model>_cover.png` exist under
| Linux | `$XDG_CONFIG_HOME/OrcaSlicer`, or `~/.config/OrcaSlicer` when unset |
| Windows | `%APPDATA%\OrcaSlicer` |
A portable `data_dir` next to the executable takes precedence. Use a separate test configuration
for a clean-install check; preserve the normal configuration and user presets.
## Cross-platform paths
Match the exact case of each `sub_path` and asset filename; Linux filesystems commonly distinguish
case even when a macOS or Windows checkout does not. Preset-name references are case-sensitive
on every platform. Avoid Windows-invalid characters (`< > : " | ? *`), reserved device names
such as `CON` / `NUL` (including with extensions), and trailing spaces or dots in path components.
Keep stems tidy too, but a space immediately before `.json` is not a trailing path-component space.
## Error → remedy
| Message | Fix |
| --- | --- |
| `can not find inherits <parent> for <preset>` | parent missing, unregistered, or listed **after** the child |
| `can not find filament_id for <name>` | nothing in the chain declares one — run `generate-id` |
| `can not find parent <name> for config <user preset>!` | a shipped name disappeared — add `renamed_from` |
| `Missing instantiation attribute for <name>` | key absent **or** not the string `"true"`/`"false"` |
| `contains incorrect keys: <keys>, which were removed` | a key valid for a different preset type |
| `defines invalid printer variant "<v>"` | not in the model's `nozzle_diameter` list |
| `has printer_variant "<v>" that does not match its nozzle_diameter` | the set comparison in [machine-profiles.md](machine-profiles.md) |
| `references unknown compatible_printers "<p>"` | the printer was renamed or deleted; fix the reference |
| `references renamed compatible_printers "<old>" (now "<new>")` | in-tree references must name the current preset; `renamed_from` does not excuse them |
| `Filament preset "<f>" is missing compatible_printers setting` | non-library filaments need a non-empty list in their **own** file — the flattened-vs-own-key trap is in [filament-profiles.md](filament-profiles.md#compatible_printers) |
| `Ambiguous AMS filament match: N presets share filament_id "X" … printer "Y"` | make the lists disjoint, or fix an `inherits` pointing at another material's `@base` |
| `[ERROR] … no <V>.json list references it, so it never loads` | `update-index`, or delete the file |
| `[ERROR] … references it and it declares no profile type` | set the correct `type` explicitly, then `normalize` and `update-index` |
| `[ERROR] … normalize would <change>` / `<V>.json: update-index would rebuild <lists>` | run that command and commit the result |
| `[ERROR] <V> has N <type> profiles named "<name>"` | identify the intended preset and remove or rename the duplicate; use `trim --dry-run` only for deliberate unindexed-file cleanup |
| `[ERROR] … must not have a setting_id` / `is missing a setting_id` | `generate-id --setting-id` |
| `inherits filament_id "X" but its own triple … mints "Y"` | `generate-id` will **not** fix this — see [ids.md](ids.md) |
| `vendor <V>'s config version: <s> invalid` | the `version` string is not Semver-parseable |
| `[json.exception.type_error.302] type must be string` | locate the non-string value in the index or model; see [failure scopes](vendor-bundle.md#failure-modes-ranked-by-blast-radius) |
| `Printer "<p>" fell back to a default preset` | final process or filament selection is a generic Default preset; check named defaults, visibility and available compatible presets. An incompatible default may instead be replaced without this error |
| `Printer "<p>" sliced but the filament change never fired` | `change_filament_gcode` never expanded |
## CI
`.github/workflows/check_profiles.yml`, job **"Check profiles"**, on `pull_request` into `main` or
`release/*`, paths `resources/profiles/**`, `resources/printers/**`, `scripts/**` and the workflow itself.
There is no push trigger — a direct push to main runs no profile validation.
The job opens with `python3 -m unittest discover -s scripts/tests -t scripts`, the tool's own unit
tests. That step is deliberately **not**`continue-on-error`: a broken tool makes everything it then says
about the profiles worthless. Every check after it is `continue-on-error` with a final gate, so one run
reports all five results. On failure a second workflow posts or replaces a single PR comment marked
`<!-- profile-validation-comment -->`, with each failing log truncated to 30 KB; it deletes the comment
once the run is green.
The job name is also the required check for the delegated-merge bot, which lets a vendor maintainer
self-merge a `resources/profiles/<Their vendor>/` PR with no human review — so whatever CI does not check
is what ships unreviewed. Its denied patterns refuse `^scripts/` and any `.py`, so a PR that touches the
| `name` | the preset name; the filename is *not* authoritative |
| `inherits` | the parent's exact `name` — no path, no `.json` |
| `instantiation` | the **string**`"true"` (selectable) or `"false"` (base) |
| `from` | `"system"` by convention; the vendor loader never reads it |
| `setting_id` | required on instantiated presets, forbidden on bases — generated |
| `renamed_from` | `;`-separated list of old names this preset supersedes |
These are config-preset keys; `machine_model` records have their own
[schema](machine-profiles.md#a-machine_model-is-not-a-config-preset). Keep `from` as `"system"`
for shipped presets. The vendor loader ignores it, but the CLI config-file loader accepts only
`system`, `user` or `User` and handles their inheritance differently.
`instantiation` is the one metadata key that is hard-gated: a missing key or any value other than the
strings `"true"`/`"false"` is an error (`Missing instantiation attribute for <name>`). A JSON boolean
`true` fails harder — it throws inside `load_from_json` and takes the **whole vendor bundle** down.
### `inherits`
Resolution is an exact-name lookup **within the same bundle**, plus one exception: filaments may inherit
from `OrcaFilamentLibrary`, which is loaded first and becomes the base bundle. Vendor-to-vendor
inheritance always fails. You can inherit from an instantiated preset as well as from a base; it is
common.
### `renamed_from`
One JSON string, `;`-separated for several old names.
- Write `"A;B"`, never `"A ; B"` — an unquoted item keeps its trailing space and can never match.
- When `renamed_from` is **absent** and the name contains `@`, the loader auto-adds the `@`-removed form
(`X @Y` → `X Y`) as a rename alias. Declaring an explicit `renamed_from`**suppresses** that, so a
preset that needs both the `@`-removed form and a real old name must list both. No shipped profile
currently does, which means any preset that gained a `renamed_from` quietly lost its `X Y` alias.
- It rescues names stored **outside** the tree: user presets and 3MF projects. It does **not** rescue
in-tree `inherits` (exact lookup), it does **not** satisfy `check_name_consistency`, the validator
reports an in-tree reference that only resolves through it (`references renamed compatible_printers
"OLD" (now "NEW")`), and `machine_model` records never read it at all.
- Only one preset may claim a given old name — two that do is a counted error
(`… was marked as renamed from "Y" … as well`). But the redirect is **inert while a live preset still
carries that name**, and nothing checks *that*; Z-Bolt ships a folder of such dead entries.
## Failure modes, ranked by blast radius
| Scope | Cause |
| --- | --- |
| **All vendors, zero system profiles** | a non-string `version`, `name` or `url` at the top level of a vendor index (`"version": 2`), or non-string `nozzle_diameter` on a model — `nlohmann::type_error` escapes the per-vendor `std::runtime_error` catch |
| **The whole vendor bundle** | unparseable `version`; index JSON parse error; a `sub_path` file missing or unparseable; unresolvable `inherits`; duplicate preset name within the vendor; empty/unknown `printer_model` or `printer_variant`; a filament resolving no `filament_id` |
| **One preset** | `instantiation` missing or a wrong string; keys belonging to another preset type (`contains incorrect keys: …, which were removed`); a non-string inside a `*_list` entry (`invalid value type for <key>`) |
| **Logged, not counted** | a raw JSON number in a preset — `invalid json type for <key>`, the value is dropped and the exit code stays 0 |
| **Nothing reported by the loader** | unregistered file; misspelled setting key; missing bed/hotend asset. Only the first of those is a `check` error; the other two reach users |
Deleting a file the index still lists surfaces as a *parse error* on line 1, not "file not found" — the
loader `ifstream`s the missing path and nlohmann reports `unexpected end of input`.
Preset names are a **single global namespace across every vendor**: a duplicate within one vendor is a
hard bundle failure, a duplicate across vendors is reported as `Found duplicated preset: <name> in
vendor: <vendor>` and still counts as an error. `check_preset_name_uniqueness` catches the within-bundle
case earlier and more precisely — including an *unindexed* twin, which is one `sub_path` edit away from
silently becoming the parent every child resolves to (`std::map::emplace` keeps the first insertion, so
index order decides). Base names, by contrast, repeat across bundles by design: `fdm_process_common`
exists in nearly all of them.
## Starting a whole new vendor bundle
Nothing generates one; copy the smallest bundle that resembles the hardware. **`Voxelab` or `M3D`** are
the minimal shape — a shared machine base, the model, one variant, a shared process base, two
processes, and an empty `filament_list` that takes the library generics. Do *not* start from `Phrozen`:
it carries local `fdm_filament_*` copies that have drifted from the library, and a filament preset that
restates most of its parent — the style this skill advises against.
Write the machine files **last**, so you only visit them once:
description:Something behaves incorrectly while Orca Slicer keeps running
labels:["bug"]
body:
- type:markdown
@@ -10,6 +10,8 @@ body:
Please note that this is not the place to make feature requests or ask for help.
For this, please use the [Feature request](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=feature_request.yml) issue type or you can discuss your idea on our [Discord server](https://discord.gg/P4VE9UY9gJ) with others.
If Orca Slicer closes on its own, freezes or stops responding, please use the [Crash report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=crash_report.yml) form instead. It asks for the logs a crash needs.
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
- type:checkboxes
attributes:
@@ -32,14 +34,22 @@ body:
attributes:
label:OrcaSlicer Version
description:Which version of Orca Slicer are you running? You can see the full version in `Help` -> `About Orca Slicer`.
placeholder:e.g. 1.9.0
placeholder:e.g. 2.5.0
validations:
required:true
- type:input
id:working_version
attributes:
label:Regression compared to a previous version
description:Did it work in a previous version?
placeholder:e.g. 2.3.2
validations:
required:false
- type:dropdown
id:os_type
attributes:
label:"Operating System (OS)"
description:"What OSes are you are experiencing issues on?"
description:"What OSes are you experiencing issues on?"
multiple:true
options:
- Linux
@@ -78,7 +88,7 @@ body:
id:reproduce_steps
attributes:
label:How to reproduce
description:Please described the detailed steps to reproduce this issue
description:Please describe the detailed steps to reproduce this issue
placeholder:|
1. Go to '...'
2. Click on '...'
@@ -100,28 +110,23 @@ body:
description:What should happen after the above steps?
validations:
required:true
- type:markdown
id:file_required
attributes:
value:|
Please be sure to add the following files:
* Please upload a ZIP archive containing the **project file** used when the problem arise. Please export it just before or after the problem occurs. Even if you did nothing and/or there is no object, export it! (We need the configurations in project file).
You can export the project file from the application menu in `File`->`Save project as...`, then zip it
* A **log file** for crashes and similar issues.
You can find your log file here:
Windows: `%APPDATA%\OrcaSlicer\log` or usually `C:\Users\<your username>\AppData\Roaming\OrcaSlicer\log`
If Orca Slicer still starts, you can also reach this directory from the application menu in `Help` -> `Show Configuration Folder`
You can zip the log directory, or just select the newest logs when this issue happens, and zip them
- type:textarea
id:file_uploads
attributes:
label:Project file & Debug log uploads
description:Drop the project file and debug log here
description:|
Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
* The **project file** used when the problem happened, zipped. Export it just before or after the problem occurs. Even if you did nothing and there is no object on the plate, export it, since we need the configuration it carries. `File` -> `Save project as...`
* The **log folder**, zipped. `Help` -> `Show Configuration Folder` opens it, or find it at:
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
* If the zip comes out over 25 MB, attach the newest logs from that folder on their own instead.
placeholder:|
Project File: `File` -> `Save project as...` then zip it & drop it here
Log File: `Help` -> `Show Configuration Folder`, then zip the log directory, or just select the newest logs in `log` when this issue happens and zip them, then drop the zip file here
Zipped project file
Zipped log folder
validations:
required:true
- type:checkboxes
@@ -136,7 +141,5 @@ body:
label:Anything else?
description:|
Screenshots? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
description:Orca Slicer closes on its own, freezes or stops responding
labels:["crash"]
body:
- type:markdown
attributes:
value:|
**Thank you for taking the time to report a crash.**
Use this form when Orca Slicer closes on its own, freezes, or stops responding.
If the application stays open and only produces a wrong result, please use the [Bug report](https://github.com/OrcaSlicer/OrcaSlicer/issues/new?assignees=&labels=&projects=&template=bug_report.yml) form instead.
A printer whose toolhead collides with the print is also a bug report rather than a crash, since the application itself did not stop.
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
- type:checkboxes
attributes:
label:Is this crash reproducible in the latest nightly build?
description:>
Please verify this crash still happens in the latest nightly build first. It may already be fixed there:
description:Pick the point where Orca Slicer stops working.
options:
- Not sure
- Onstartup, before the main window appears
- When opening or importing a project or model
- While changing printer, filament or process settings
- While slicing
- In the 3D view, Preview or Assembly view
- When exporting G-code or sending a print to the printer
- Onthe Device tab, or connecting to a printer (camera, sync, login)
- While using a specific tool, dialog or calibration
- After resuming from sleep or changing monitors
- When closing the application
- Noclear pattern
validations:
required:true
- type:dropdown
id:crash_frequency
attributes:
label:How often does it happen?
options:
- Not sure
- Every time
- Often, but not every time
- Rarely
- It only happened once
validations:
required:true
- type:dropdown
id:fresh_config
attributes:
label:Does it still crash with a fresh configuration?
description:>
Close Orca Slicer and rename your configuration folder (`%APPDATA%\OrcaSlicer` on Windows,
`$HOME/Library/Application Support/OrcaSlicer` on macOS, `$HOME/.config/OrcaSlicer` on Linux),
then start it again. Renaming keeps your settings, so you can put the folder back afterwards.
options:
- I have not tried this
- Yes,it still crashes
- No,the crash goes away
validations:
required:true
- type:textarea
id:reproduce_steps
attributes:
label:How to reproduce
description:Please describe the detailed steps that lead to the crash.
placeholder:|
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. Orca Slicer closes
validations:
required:true
- type:textarea
id:system_info
attributes:
label:Additional system information
description:>
Display card and driver version are worth adding for crashes on startup or in the 3D view.
CPU and memory are worth adding for crashes while slicing.
placeholder:|
CPU: 11th gen Intel r core tm i7-1185g7/AMD Ryzen 7 6800h/...
Memory: 32/16 GB...
Display Card: NVIDIA Quadro P400/...
validations:
required:false
- type:textarea
id:file_uploads
attributes:
label:Project file, logs and crash report uploads
description:|
A crash report without logs usually cannot be acted on. Attach the files with the **Paste, drop, or click to add files** control directly underneath this box. Zip anything that is not a `.log`, `.txt` or image, since GitHub rejects other file types, and keep each file under 25 MB.
* The **project file** used when the crash happened, zipped. Export it just before or after the crash, even if the plate is empty, since we need the configuration it carries. `File` -> `Save project as...`
* The whole **log folder**, zipped rather than single files picked out of it. `Help` -> `Show Configuration Folder` opens it, or find it at:
* Windows: `%APPDATA%\OrcaSlicer\log`, usually `C:\Users\<you>\AppData\Roaming\OrcaSlicer\log`
* On Windows the crash itself is written to a separate `crash_*.log` in there, and that is the file we need most. If the zip comes out over 25 MB GitHub will refuse it, so attach the newest log and any `crash_*.log` on their own instead.
* The **operating system crash report**, on macOS and Linux, where Orca Slicer cannot write its own crash log. It is often the only record of where it died:
* macOS: Console.app -> Crash Reports, or `$HOME/Library/Logs/DiagnosticReports/`. The file starts with `OrcaSlicer` and ends in `.ips`. Zip it before attaching, GitHub does not accept `.ips` files.
* Linux: run `orca-slicer` from a terminal (Flatpak: `flatpak run com.orcaslicer.OrcaSlicer`) and paste everything it prints when it dies. On systemd systems `coredumpctl info orca-slicer` gives a backtrace.
placeholder:|
Zipped project file
Zipped log folder
Zipped macOS .ips crash report, or the terminal output on Linux
validations:
required:true
- type:checkboxes
id:file_checklist
attributes:
label:Checklist of files to include
options:
- label:Log folder
- label:Project file
- label:Operating system crash report (macOS and Linux)
- type:textarea
attributes:
label:Anything else?
description:|
Screenshots? References? Anything that will give us more context about the crash you are encountering!
head -c 30000 ${{ runner.temp }}/validate_slice.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_filament_subtypes.outcome }}" = "failure" ]; then
echo "### Filament Subtype Validation Failed"
echo ""
echo '```'
head -c 30000 ${{ runner.temp }}/validate_filament_subtypes.log || echo "No output captured"
echo '```'
echo ""
fi
if [ "${{ steps.validate_custom.outcome }}" = "failure" ]; then
echo "### Custom Preset Validation Failed"
echo ""
@@ -147,13 +252,11 @@ jobs:
fi
echo "---"
echo "*Please fix the above errors and push a new commit.*"
echo '*Fix the errors above and push a new commit. To reproduce this run locally: `scripts/check_profile.sh`, or `scripts\check_profile.bat` on Windows.*'
core.error(`@${commenter} attempted a delegated merge with non-regular files: ${irregularFiles.join(', ')}`);
return refuse(
`it adds symlinks, submodules or files I cannot verify:\n\n${formatList(irregularFiles)}\n\nA maintainer should look at this before it goes any further.`
);
}
// ---- mergeability: waits for GitHub to compute it ----
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
```bash
cd build && ctest --output-on-failure # all tests
ctest --test-dir ./tests/libslic3r # individual suite
ctest --test-dir ./tests/fff_print
cd build && ctest -C Release --output-on-failure # all tests
ctest --test-dir ./tests/libslic3r -C Release# individual suite
ctest --test-dir ./tests/fff_print -C Release
```
## Documentation
- Docs live in `docs/`; the high-level design of a subsystem goes in `docs/HLSD/<subsystem>.md`.
- Describe the design as it stands — what the subsystem does, why it exists, and the constraints that shape it. Not the route that got there: no phases, task lists, status markers, or "before/after this PR" framing.
- Planning and investigation output (brainstorms, superpowers design and plan docs) stays in `docs/superpowers/`, which is gitignored. Never commit it.
- Write a doc only when the design is not evident from the code, and when a change invalidates an existing one, update it in the same PR.
-`#pragma once` for headers. Smart pointers and RAII preferred
- Parallelization via TBB — be mindful of shared state
- Always use `SetSizerAndFit(sizer)` instead of `SetSizer(sizer)` on top level window. Unless `SetSizer` must be called before the full layout is built, call `sizer->SetSizeHints(window)` afterwards in this case.
- Add helper functions or utilities only when existing code cannot reasonably be reused. Avoid duplication.
- Keep code concise and clear. Manually simplify AI generated bloated codes before review.
- Include targeted tests or documented verification for behavior changes, especially in slicing logic, profiles, formats, and GUI defaults.
- For profile changes (`resources/profiles/<Vendor>/**`), check that `version` in the sibling `resources/profiles/<Vendor>.json` was bumped.
- For translation changes (`localization/i18n/**/*.po`), check that recurring terms match the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) for that language.
## Localization & translations
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_guide.md) for the human-facing version of these principles.
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/developer_reference/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
### Editing rules
- Only edit `msgstr` — **never** change `msgid`, and never "fix" wrong English in the translation alone. Report the source string instead.
- Preserve exactly: placeholders (`%s`, `%d`, `%1%`, `%zu`, `%%`), every `\n` (count *and* position, including leading/trailing), leading/trailing spaces, HTML tags, `℃`, and the file's encoding and line endings.
- **Never reorder positional arguments** in a `c-format` string. If the msgid is `%d` then `%s`, that order must hold — swapping them breaks at runtime.
-`msgctxt` separates homonyms — always read it. `Back`/`Camera View` is the rear view of the 3D navigator, while `Back`/`Navigation` is the go-back button; `Top` exists in the *Alignment*, *Layers* and *Camera View* senses.
- When a string needs disambiguating, add context in the source (`_L_CONTEXT`/`_u8L_CONTEXT`), don't work around it in the translation.
- A literal `%` inside a string xgettext flagged `possible-c-format` will fail `msgfmt`. Fix it with a `// xgettext:no-c-format, no-boost-format` comment above the string in the source — do not mangle the translation or use `%%` in text that is never passed through printf.
- Plural entries: read `nplurals` from the catalog's `Plural-Forms` header (it is **not** always 2 — ja/ko/zh/th/vi use 1, ru/cs/pl/lt use 3, uk uses 4). Each form must be genuinely inflected for its quantity; repeating one sentence across all forms is a bug in Slavic/Baltic languages, though it is correct for Turkish and Hungarian.
- An entry whose `msgstr` equals its `msgid` is untranslated even though it is not empty; a plural entry with any empty form is likewise incomplete.
- Mark machine-produced translations with an `# AI Translated` translator comment. Don't add it to a human translation you didn't actually rewrite.
- Don't reflow or re-wrap unrelated entries — keep the diff limited to the strings you changed.
### Verifying
-`scripts/run_gettext.bat --full` (Windows) regenerates the template, merges every catalog and compiles the `.mo` files. It must exit 0.
- Or check a single catalog with `msgfmt --check-format -o <out>.mo localization/i18n/<lang>/OrcaSlicer_<lang>.po`.
- Fuzzy entries are not shown to users. If you correct one, clear its `fuzzy` flag, otherwise the fix never ships.
@@ -85,26 +89,41 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
### Belt Printer Builds
The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
# How to install
## Windows
Download the **Windows Installer exe** for your preferred version from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases).
Download the **Windows Installer exe** for your preferred version from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases). Both `x64` and `arm64` installers are published — pick the one matching your CPU.
- *For convenience there is also a portable build available.*
- *For convenience there is also a portable build available.*
<details>
<summary>Troubleshooting</summary>
- *If you have troubles to run the build, you might need to install following runtimes:*
- [Alternative Download Link Hosted by Microsoft](https://aka.ms/vs/17/release/vc_redist.x64.exe)
- This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142`
- *If you have troubles to run the build, you might need to install following runtimes:*
- [Alternative Download Link Hosted by Microsoft](https://aka.ms/vs/17/release/vc_redist.x64.exe)
- This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142`
</details>
Windows Package Manager
### Microsoft Store
Install from the [Microsoft Store](https://apps.microsoft.com/detail/9mv6gl23xm59) when you prefer a Store-signed package (helps on Windows 11 Smart App Control).
The [Homebrew cask](https://formulae.brew.sh/cask/orcaslicer) installs the official macOS DMG from [GitHub Releases](https://github.com/OrcaSlicer/OrcaSlicer/releases).
## Linux
### Flathub (Recommended)
OrcaSlicer is available through FlatHub:
<a href='https://flathub.org/apps/com.orcaslicer.OrcaSlicer'><img width='240' alt='Download on Flathub' src='https://dl.flathub.org/assets/badges/flathub-badge-en.png'/></a>
@@ -154,6 +182,9 @@ flatpak run com.orcaslicer.OrcaSlicer
It can also be installed through graphical software managers (KDE Discover, GNOME Software, etc.) when Flathub is enabled. Search for **OrcaSlicer** in your software center.
### AppImage
AppImages are published for both **x86_64** and **aarch64** (ARM64). Pick the file matching your CPU — the ARM64 build has `aarch64` in its name (e.g. `OrcaSlicer_Linux_AppImage_Ubuntu2404_aarch64_*.AppImage`).
1. Download App image from the [releases page](https://github.com/OrcaSlicer/OrcaSlicer/releases).
2. Double click the downloaded file to run it.
@@ -181,11 +212,11 @@ resolution: 0.1
# Supports
**OrcaSlicer** is an open-source project and I'm deeply grateful to all my sponsors and backers.
Their generous support enables me to purchase filaments and other essential 3D printing materials for the project.
**OrcaSlicer** is an open-source project, and we're deeply grateful to all our sponsors and backers.
Their generous support helps fund filaments and other essential 3D printing materials for the project.
@@ -221,6 +252,7 @@ OrcaSlicer began in that same spirit, drawing from BambuStudio, PrusaSlicer, and
The OrcaSlicer logo was designed by community member [Justin Levine](https://github.com/jal-co).
# License
- **OrcaSlicer** is licensed under the GNU Affero General Public License, version 3.
- The **GNU Affero General Public License**, version 3 ensures that if you use any part of this software in any way (even behind a web server), your software must be released under the same license.
- OrcaSlicer includes a **pressure advance calibration pattern test** adapted from Andrew Ellis' generator, which is licensed under GNU General Public License, version 3. Ellis' generator is itself adapted from a generator developed by Sineos for Marlin, which is licensed under GNU General Public License, version 3.
set(DEP_DOWNLOAD_DIR${CMAKE_CURRENT_SOURCE_DIR}/DL_CACHECACHEPATH"Path for downloaded source packages.")
set(FLATPAKFALSECACHEBOOL"Toggles various build settings for flatpak, like /usr/local in DESTDIR or not building wxwidgets")
option(SLIC3R_CAD"Build the SolveSpace solver and OCCT ModelingAlgorithms module the parametric Design/CAD tab needs. Must match the main project's SLIC3R_CAD."ON)
DO NOT ADD CODE WITH OTHER EXTERNAL DEPENDENCIES TO THIS DIRECTORY.
Read on:
pybind11_conduit_v1.h — Type-safe interoperability between different
independent Python/C++ bindings systems.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.