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
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
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
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
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
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
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
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
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
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
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 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
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
- 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
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
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
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