Skip to content

Fix model on Linux and macOS: CGAL repair instead of the Netfabb service (OrcaSlicer#12155) - #991

Draft
alex-mextner wants to merge 10 commits into
Snapmaker:mainfrom
alex-mextner:upstream/fix-model-cgal
Draft

alex-mextner wants to merge 10 commits into
Snapmaker:mainfrom
alex-mextner:upstream/fix-model-cgal

Conversation

@alex-mextner

@alex-mextner alex-mextner commented Oct 11, 2026 •

Copy link
Copy Markdown

Description

What. "Fix model" now works on Linux and macOS, and Windows uses the same code. The Windows 10 SDK (Netfabb) fixer is replaced by a local CGAL repair. This ports OrcaSlicer#12155 and fixes the repair problems found while testing it.

Why.

  • Today Fix model exists only on Windows. On Linux and macOS a broken mesh only shows the tip ""Fix Model" feature is currently only on Windows".
  • On Windows it called Microsoft's Netfabb-based online repair service through the Windows 10 3D printing SDK. Microsoft ended that service in 2022.
  • The new repair runs locally with CGAL, which Snapmaker Orca already bundles (5.4). It adds no dependency and needs no network.

Commits

  1. Multi OS fix model with CGAL (author @ealap): CGAL Fix Model: New for Linux and Mac OrcaSlicer/OrcaSlicer#12155 squashed, taken from ealap's Snapmaker port. Removes FixModelByWin10.*, adds FixModelByCgal.* and MeshBoolean::cgal::repair(), and enables the menu item on every OS.

  2. Build the CGAL fix model with CGAL 5.4 and move its worker into libslic3r:

    • CGAL 5.4 build fixes. 5.4 has no triangulate_and_refine_hole() overload without output iterators and needs border.h / triangulate_hole.h included.
    • Port of fix: prevent heap corruption when repairing models with auto-backup OrcaSlicer/OrcaSlicer#15395: the worker holds a SaveObjectGaurd, so auto-backup does not clone the object while it changes (heap corruption).
    • The repair loop moves unchanged into src/libslic3r/ModelObjectRepair.* so it can be unit tested.
    • localization/i18n/list.txt swaps FixModelByWin10.cpp for FixModelByCgal.cpp and adds ModelObjectRepair.cpp.
  3. Fix CGAL fix model repair and the volume loop, with tests. repair() now:

    • welds near-coincident points (STEP seams);
    • orients the triangle soup before building the mesh;
    • fills holes before resolving self-intersections, and fills holes pinched at a vertex one loop at a time;
    • replaces the self-union corefine_and_compute_union(m, m) with a union of the solid shells minus the cavities (CGAL needs distinct inputs without self-intersections);
    • always orients the result outwards;
    • fails when the result encloses no volume.

    The volume loop:

    • no longer skips the volume after a dropped flat volume;
    • rejects invalid volume indices;
    • rejects objects without any 3D part before changing them;
    • honours cancel.
  4. Fix model: leave clean volumes untouched, keep nested solids, repair a copy off the GUI thread:

    • A volume with no open edges and no self-intersections is not touched, so its paint is kept.
    • Overlapping shells are resolved by nesting level (solid − (cavity − (island − …))), so a solid inside a cavity survives.
    • The worker repairs a copy of the object. The result is applied on the GUI thread, so cancel or failure leaves the object unchanged.
    • When self-intersections remain after a repair, the user gets a warning instead of "repaired".
  5. Delete the commented-out Netfabb build settings. CGAL Fix Model: New for Linux and Mac OrcaSlicer/OrcaSlicer#12155 commented out the WIN10SDK CMake block and the CI env; Orca reviewers asked to delete such code instead. The three Windows .bat scripts no longer pass the unused -DWIN10SDK_PATH.

  6. Add the new Fix model messages to the catalog, with zh_CN translations. Five new strings go into Snapmaker_Orca.pot and zh_CN. The Chinese texts are machine translations, marked # AI Translated, and need a native-speaker review. Other languages show English for these strings until the next translation update.

  7. Fix model: skip the weld where its grid would overflow, reset the report on a crash. The weld grid uses int64 cells of coordinate / tolerance, so it is now skipped once that ratio reaches 1e15 (a test repairs a part 2^54 mm from the origin). The self_intersections_remain out-parameter is cleared before the guarded call.

  8. Fix model: record the repaired mesh for the project backup. apply_repaired_volumes() deletes the dropped volumes itself, so it now calls save_object_mesh() like ModelObject::delete_volume() does. No unit test covers this: the tests run without the project backup.

  9. Fix model: find the split parts by pointer, and stop split() using a deleted volume, plus a follow-up commit that keeps the volume addresses as uintptr_t, so deleted volumes leave no invalid pointer values to compare.

    • ModelVolume::split() also deletes other volumes of the object whose convex hull is degenerate (flat sheets), wherever they are. The repair loop then counted the wrong volumes as the parts of the split volume. It now finds them as the volumes that were neither before nor after it, compared by address: delete_volume() gives the last remaining volume a new ObjectID, so ids would mistake an unrelated volume for a part.
    • ModelVolume::split() deletes the volume it was called on when its first shell is flat, and then kept reading this->object (use-after-free). This existed before this PR; Fix model reaches it whenever the first shell of a broken volume is flat. The discard loop now keeps the owning object in a local.
    • Four new [ModelObjectRepair] cases: a selected volume whose split drops a flat volume before it; a clean volume whose tiny mesh is scaled up; a volume whose first shell is flat; a volume whose shells are all flat next to one other volume.

The commits cherry-picked onto main (441f73c) with no conflicts.

Behaviour notes

  • Only broken volumes change. A broken volume made of several shells is first split into parts. Parts that enclose no volume (flat sheets) are dropped. Parts with open edges or self-intersections are repaired.
  • Paint (colour, support, seam) on a part that really gets repaired or split is lost. Clean volumes keep their paint. Remapping paint (Keep painting after cut OrcaSlicer/OrcaSlicer#13472) is out of scope.
  • Fix model creates an undo snapshot, and cancel leaves the object unchanged.
  • If self-intersections remain, the notification says "Repaired, but self-intersections remain in: …".
  • Windows users lose the Netfabb service, but that service no longer works anyway.

Remaining risks (known, not addressed here)

  • The repair runs inside try_catch_signal({SIGSEGV, SIGFPE}), but that only traps on Windows. On Linux and macOS a crash inside CGAL still ends the app.
  • Overlapping closed shells in one volume are split into overlapping parts without a warning. This is upstream behaviour; the slicer unions the parts per layer, and a test pins it.
  • The nesting test uses one probe vertex per shell. A shell that touches its container exactly at that vertex is misclassified.
  • Nesting levels come from geometry, not from the input orientation, so unusual inputs are read as cavities at odd levels.
  • Copying the object costs time on very large painted objects. GUI code that keeps pointers to replaced volumes is only covered by the end-to-end GUI run below.
  • resources/data/hints.ini still says Fix model works "on the Windows system". It is left as upstream has it, because changing it would invalidate that hint's translation in every language.

Not tested: Windows and macOS builds and runs (no machines; Snapmaker CI does not build PRs). The code paths are OS-independent apart from the build-script cleanup.

Related:

Credits:

Screenshots/Recordings/Graphs

These shots come from this branch's AppImage (head 87dc3e2, built on main as described under Tests). A scripted GUI test drives the app on Xvfb. The model is a real user project: 11 copies of a part imported from a Creo surface STEP with the old importer, so every copy has open edges. The model itself is not attached because its licence is unknown.

Before: every copy shows the warning icon and non-manifold edges.
before

After Fix model on all objects: no warnings, and the notification lists the repaired objects.
after fix

After auto-arrange:
after arrange

Sliced:
after slice

Tests

Current head 87dc3e2 (commits 1–9), Snapmaker-main build: Ubuntu 26.04 on a Runpod cpu5c pod with 16 vCPU, running build_linux.sh -sitr (dependencies from a cache built from the same deps/ tree) and then ctest -C Release -j1 --output-on-failure -E 'render_realtime_body|rate limit throttles beyond cap'.

  • ctest: 579 of 579 passed, among them all 24 [ModelObjectRepair] and 27 [MeshBoolean] cases. The three excluded SnapLogClient tests are the two render_realtime_body SIGSEGVs that main itself has and the flaky timing test (see below and the earlier head).
  • The AppImage was built (sha256 7e8f7b87…7d688).
  • GUI run on this AppImage: PASS (the scripted test under "GUI test" below; it produced the screenshots above). The saved project has 11 build items, each with 0 open edges and consistent winding, watertight, volume 3143.35 mm³, size 25.98 × 22.50 × 10.50 mm, inside the bed and without overlaps.
  • Unrelated to this PR, seen on the way: on a machine without an X video sink for GStreamer (Ubuntu's gstreamer1.0-x, part of a normal desktop install), main crashes at startup in the wxMediaCtrl2 constructor (StatusBasePanel::create_monitoring_page), because wxMediaCtrl then has no backend. The test machine has that package installed, as a desktop does.

Everything below is evidence for earlier heads, labelled as such.

Snapmaker-main build of the earlier head 21d7aa4 (commits 1–8, without commit 9): Ubuntu 26.04 on a Runpod cpu3c pod with 32 vCPU, running build_linux.sh -dsitr and then ctest -C Release -j1 --output-on-failure.

  • Full ctest: 581 of 583 passed. The only failures were the two render_realtime_body SIGSEGVs that main itself has (see below). All 47 [MeshBoolean] and [ModelObjectRepair] cases passed.

  • A second run with those two tests excluded hit a flaky timing test in SnapLogClient (log(): rate limit throttles beyond cap), which passed in the first run.

  • The final run excluded that test as well: 575 of 575 passed, and the AppImage was built (sha256 8bfe0f49…d573c).

  • Red run, same pod. src/libslic3r/MeshBoolean.cpp and ModelObjectRepair.cpp were reverted to the ported upstream repair, with only the new API stubbed and the tests kept. 21 Fix model tests fail, among them:

    • many small holes; inside-out mesh; inverted face; seams split by noise;
    • solids touching at a vertex or sharing an edge;
    • overlapping shells (closed or open), cavities and islands;
    • self-intersection reports; a surface that encloses no volume;
    • the dropped-flat-volume loop; an object left without volumes; flat-only objects; invalid indices;
    • leaving clean multi-shell volumes as they are;
    • applying a repaired copy.

    The other 26 cases pass on red: they are guards. Besides those 21, three SnapLogClient tests failed on red, all unrelated: the two known SIGSEGVs and one flaky test.

  • A GUI run of this earlier head did not get past startup (see the current head above).

Evidence from the fork (Ubuntu 26.04, Runpod CPU pods, uncached dependencies, build_linux.sh -dsitr, then ctest -C Release -j1 --output-on-failure):

Tree Fix model code Result
fork fixmodel/ubuntu f773e60 commits 1–4 ctest 554/554 passed
fork ubuntu-26.04 a75a248 (release v2.4.0-linux.2) commits 1–4 plus other fork changes ctest 557/557 passed; GUI Fix model run PASS

New tests (Catch2, in tests/libslic3r):

  • test_meshboolean.cpp, [MeshBoolean]: 26 new cases. They cover:
    • empty, closed and hollow solids, which stay unchanged;
    • missing faces and several separate holes;
    • a large hole in a curved surface;
    • many small holes pinched at a vertex;
    • an inside-out mesh and a single inverted face;
    • an unindexed soup, and seams split by sub-micron noise;
    • duplicated and degenerate faces;
    • solids touching at a vertex or sharing an edge;
    • overlapping shells (closed, or one of them open), keeping cavities and islands inside cavities;
    • reporting the self-intersections that cannot be removed;
    • sub-millimetre parts, parts far from the origin, and parts too far to weld;
    • failure for a surface that encloses no volume.
  • test_model_object_repair.cpp (new), [ModelObjectRepair]: 24 cases. They cover:
    • repairing a whole object or a single volume;
    • split volumes, and dropping flat parts, also when the split deletes flat volumes elsewhere in the object or the split volume itself;
    • leaving a clean volume alone even when its tiny mesh is scaled up;
    • never leaving an object without volumes, and rejecting invalid volume indices;
    • cancellation and progress reporting;
    • leaving clean multi-shell volumes as they are, painted ones included;
    • the self-intersection report;
    • applying a repaired copy, and cancelling a copy.

Red → green, on the fork. Each red run used the new tests on top of the ported upstream repair. Logs are kept locally.

Round Failing on red Typical red messages Green
repair + loop fixes 12 of 539 an inside-out mesh stayed inside-out (-1000 > 0); self-union lost volume (1833.33 vs 2000); overlapping shells still self-intersecting; seams split by noise not welded (-0.00003 > 0); after a dropped flat first volume, the next volume was skipped (open_edges 3 == 0); an object was left with no volumes 539/539
review rounds 2–4 3, 1, 1 cavity filled while merging an overlapping shell (1800 vs 1675); object of flat parts changed before failing; invalid volume index accepted; stacked flat sheets; cancel during the pre-check 543/543
hardening 6 of 553, then 1 of 554 clean multi-shell volume split and its paint dropped (2 == 1); island inside a cavity erased (13400 vs 13464); repaired copy not applied; self-intersections not reported; cavity lost inside overlapping solids (11999.999 vs 11792) 554/554

The tests that already passed on red are guards: the conversion test, overlapping closed shells being split, cancelling a copy, and "no self-intersections" reports.

Known failures on main, not related to this PR. Building main (441f73c) itself fails 2 of 537 tests: slic3rutils:render_realtime_body: huge ext value truncated, eventName survives and ... per-value truncation keeps eventName in ext, both SIGSEGV in SnapLogClient.

GUI test. A scripted run on Xvfb:

  1. Open the user's 3MF.
  2. Fix model on all 11 objects.
  3. Auto-arrange, slice, then save the project.
  4. Check the saved 3MF: 11 build items, each with 0 open edges, watertight, volume 3143.3 mm³, size 25.98 × 22.50 × 10.50 mm.

The analysis arguments were --count 11 --volume 3143.3 --size 25.98,22.50,10.50.

Not tested: Windows, macOS.

ealap and others added 10 commits October 11, 2026 05:52
Replace Windows 10 SDK model fixer with CGAL-based fixing for
cross-platform support (Windows/Linux/macOS). Faster, preserves
multi-part objects, and fixes only broken parts.

Cherry-picked (squashed) from OrcaSlicer#12155.
…ic3r

Snapmaker bundles CGAL 5.4, which has no triangulate_and_refine_hole()
overload without output iterators and needs border.h / triangulate_hole.h
included explicitly.

Port OrcaSlicer#15395: hold a SaveObjectGaurd while the worker
mutates the object, so the auto-backup manager does not clone it
concurrently (heap corruption).

Move the repair loop unchanged from FixModelByCgal.cpp into
libslic3r/ModelObjectRepair so it can be unit tested; the GUI keeps the
progress dialog, cancellation and error reporting.

Refs: OrcaSlicer#12155
Co-authored-by: weng haishi <74546450+wenghaishi@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
repair(): weld near-coincident points (seams of STEP conversions), orient the soup before building the mesh, fill holes before resolving self-intersections and fill holes pinched at a vertex loop by loop, replace the self-union (CGAL requires distinct, non-self-intersecting inputs) by the union of the solid shells minus the cavities, always orient the result outwards, and fail on a result without volume.

repair_model_object(): do not skip the volume after a dropped flat first volume, reject invalid volume indices and objects without a 3D part before changing them, honour cancellation during that check, and translate the formatted messages.

Refs: OrcaSlicer#12155
…a copy off the GUI thread

repair_model_object() leaves a volume without open edges and self-intersections as it is, even when it is made of several shells: splitting it dropped its painted facets. Parts that only intersect themselves are repaired too, and the returned ModelObjectRepairReport tells whether a repaired part still intersects itself; the object list then warns instead of reporting a clean repair.

MeshBoolean::cgal::repair() resolves overlapping shells by nesting level, the longest chain of shells containing each other: level 0 minus (level 1 minus (level 2 ...)), so a solid inside a cavity survives and overlapping solids around a cavity count as one level. It re-checks self-intersections after resolving them (new optional out-parameter) and runs inside try_catch_signal like the mesh booleans (traps signals where try_catch_signal can, i.e. Windows).

fix_model_with_cgal_gui() repairs a copy of the object (Model::add_object() into a scratch model) on the worker thread and applies it on the GUI thread after join with apply_repaired_volumes(), so the progress dialog never draws a half-repaired object and a cancelled or failed repair leaves it unchanged.
The CGAL fix model replaced the Windows 10 SDK (Netfabb) fixer, so the
WIN10SDK detection in CMakeLists.txt, the Windows SDK env of the CI
build step and the -DWIN10SDK_PATH arguments of the Windows build
scripts no longer configure anything.
Adds the strings the CGAL fix model introduced to Snapmaker_Orca.pot, so
a catalog merge keeps them, and to the zh_CN catalog: "Repair failed",
"Split into %1% parts", "The object has no part that encloses a volume",
"Repaired, but self-intersections remain in" and "Use \"Fix Model\" to
repair the mesh.". The Chinese texts are machine translations, marked
"# AI Translated" for a native speaker to review.
…ort on a crash

weld_close_points() keys its grid by int64 cells of coord / tolerance,
which overflows for coordinates beyond about 9e15 tolerances; the weld
is now skipped there, and a test repairs a part 2^54 mm from the origin.
repair() clears self_intersections_remain before the guarded call, so a
trapped CGAL crash does not leave it unset.
apply_repaired_volumes() deletes the dropped volumes itself instead of
through ModelObject::delete_volume(), so a repair that only drops
volumes never called save_object_mesh() and the project backup missed
the change.
…deleted volume

ModelVolume::split() also deletes the other volumes of the object whose convex hull is degenerate, so the repair loop lost track of the parts of the volume it split: it now finds them as the volumes that were neither before nor after that volume. Pointers, not ObjectIDs: delete_volume() gives the last volume left a new id, which made an unrelated volume look like a part when every part was flat.

split() deletes the volume it is called on when its first shell is flat, and then kept reading this->object; the discard loop now keeps the owning object in a local.

Tests: a selected volume whose split drops a flat volume before it; a clean volume whose tiny mesh is scaled up; a volume whose first shell is flat; a volume whose shells are all flat next to one other volume.
A volume deleted by split() leaves an invalid pointer value, and comparing such values is undefined. The addresses are now kept as uintptr_t, as in the OrcaSlicer port.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants