Commit Graph
296 Commits
Author SHA1 Message Date
SoftFever 5a170520d3 refactor: rename SnapOrca references to Orca in CAD components to avoid confusion and update recipe versioning 2026-08-27 18:49:50 +08:00
SoftFever 0aeb6df122 Merge branch 'main' into cad-mainline 2026-08-27 17:06:37 +08:00
SoftFever 5552ed6cf1 Keep mixed-color filaments intact when the extruder count changes (#15385)
* 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.
2026-08-26 19:06:33 +08:00
Ian Bassi a5223279ac Fix uneven corner rounding in multiline infill (#15352)
* 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.
2026-08-25 12:46:34 -03:00
SoftFever bfe5f7e63c fix text error 2026-08-25 21:35:43 +08:00
Tommaso BianchiandClaude Opus 5 3fd5c3353a A reference is a reference whatever feature holds it
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
2026-08-24 19:00:45 +02:00
SoftFever e342698d8e Update sublayer option check. Add validation warning for gradient mixed filament without sublayer mixing 2026-08-24 15:30:02 +08:00
Tommaso Bianchi 6e7f6429fa A body carries its own name, because a body is not its first feature
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.
2026-08-23 19:09:17 +02:00
SoftFever 2b1499a087 clean up comments 2026-08-23 22:43:41 +08:00
SoftFever 4a32a9e066 Match mixed filament swatches to the editor's gradient preview
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.
2026-08-23 22:11:49 +08:00
SoftFever 38d51783ae Refresh the mixed filament list when a component preset changes 2026-08-23 22:11:49 +08:00
SoftFever 9985688b5b Blend mixed slots in the machine-send and AMS-sync thumbnails 2026-08-23 22:11:49 +08:00
SoftFever d27766aff0 Reject a mixed filament as the wipe tower filament 2026-08-23 22:11:49 +08:00
SoftFever 0c3d7c6ed1 Expand mixed slots in by-object filament bookkeeping 2026-08-23 22:11:49 +08:00
SoftFever 2131ef0560 Keep mixed filaments across app restarts 2026-08-23 22:11:49 +08:00
Ian Bassi 3c37bf9ca4 Import project 2026-08-23 22:11:48 +08:00
Ian Bassi 42f708bbb6 Fixes from Full spectrum port
https://github.com/OrcaSlicer/OrcaSlicer/pull/14383
2026-08-23 22:11:48 +08:00
Ian Bassi 86a7e93a48 Layer subdivision fix 2026-08-23 22:11:48 +08:00
Ian Bassi 8fea099d99 BBL Port Color Mix Base 2026-08-23 22:11:48 +08:00
Tommaso Bianchi 4693542d0d Port from snaporca: the solver's 1024-unknown cliff, and the scale rungs
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
2026-08-23 02:04:03 +02:00
Tommaso Bianchi 82db99f337 Port from snaporca: sketch-only projects save, scripted geometry arrives exact,
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
2026-08-23 01:22:32 +02:00
Tommaso BianchiandClaude Opus 5 942f6c28c7 Port: mirror emits a half that continues the chain
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>
2026-08-22 15:00:50 +02:00
Tommaso BianchiandClaude Opus 5 cbbd24dcb4 Port the exact loop area, the offset traversal fix, and the 2D sketch ladder
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>
2026-08-22 14:26:46 +02:00
Tommaso BianchiandClaude Opus 5 5d7fc8c545 Port the sketch layer work from snaporca: offset chains, right-click, MCP verbs
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>
2026-08-22 09:00:26 +02:00
Kris Austin 4b397fc2cc fix: slice the same model to the same lightning infill every time (#15311) 2026-08-21 22:24:39 -03:00
ExPikaPakaandSoftFever 5ed56eb876 Cache system presets to eliminate startup and wizard load times (#14217)
* 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>
2026-08-21 16:56:52 +08:00
Ian Bassi ca65f0fd8e Normalize the junction direction vector over XYZE (#15308)
* 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.
2026-08-20 18:50:23 -03:00
Tommaso Bianchi 52d16e4218 Merge remote-tracking branch 'prfork/cad-mainline' into cad-mainline 2026-08-20 17:45:15 +02:00
Tommaso Bianchi 2b02a8e3dd Design tab: move the CAD sources into their own folder
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.
2026-08-20 17:44:34 +02:00
Ian Bassi aaa8e98bb0 Time estimator fixes (#15304)
* 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.
2026-08-20 09:16:02 -03:00
Kris Austin 8047141981 test: fix the flaky multiline lightning smoothing assertion (#15294) 2026-08-19 11:28:58 -03:00
SoftFever 6a0524ea85 Merge branch 'main' into pr/tommasobbianchi/15238 2026-08-19 19:15:38 +08:00
Ian Bassi 322dc9b6a6 Smooth more patterns (#15205) 2026-08-18 12:19:27 -03:00
SoftFever 7d8c024da5 Merge branch 'main' into cad-mainline 2026-08-18 15:33:31 +08:00
f529692ac0 Fix slowdown for caged external overhangs (#14735)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-08-16 23:40:50 -03:00
Tommaso BianchiandClaude Opus 5 799f840218 Thicken Surface: fill the corners of a closed-loop wall
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>
2026-08-15 23:13:32 +02:00
Tommaso BianchiandClaude Opus 5 457610108e CAD tests: pin the sheet-body mass properties with the rig's own numbers (snaporca-lu27)
Ported from snaporca 35befd965c. See that commit for the full rationale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 21:10:40 +02:00
Tommaso Bianchi d8103f794b CAD: a mate needs B to have a body, so stop offering one when it does not
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.
2026-08-14 07:45:32 +02:00
Tommaso Bianchi 5889f6640f CAD: extrude a sketch region with its holes, and stop crashing at startup
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.
2026-08-13 23:43:36 +02:00
78eef79ffe Fix flushing-volume warning for single-filament plates (#14704)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-08-13 17:50:55 -03:00
SoftFever 2179f5f670 fix build errors on Windows 2026-08-13 23:26:33 +08:00
Tommaso Bianchi 358c331cc6 Merge SoftFever's main-into-cad-mainline update
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.
2026-08-13 09:44:30 +02:00
Tommaso Bianchi 0e7fcb3daf Recipe v5: length-frame every feature, so the format stops orphaning projects
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.
2026-08-13 09:36:21 +02:00
SoftFever 030e5f469e Merge branch 'main' into cad-mainline 2026-08-13 15:22:08 +08:00
Tommaso Bianchi 7245415af7 Sketch: a profile may hold more than one closed loop — a plate with a hole extrudes
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.
2026-08-13 08:49:18 +02:00
Tommaso Bianchi 6b3642fa53 Mate viability: which of the five apply, and why the others do not
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.
2026-08-12 23:45:37 +02:00
Tommaso Bianchi 9125e0b4f8 Connector face drift: warn without crying wolf — and bump the recipe version
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.
2026-08-12 23:04:49 +02:00
Tommaso Bianchi 13c702bed3 Chamfer drift is the driver's, not the kernel's — measured, not argued
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.
2026-08-12 22:36:20 +02:00
Tommaso Bianchi b9d6b59f90 A feature that destroys a body must say so, not ship a phantom
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.
2026-08-12 20:57:35 +02:00
Tommaso BianchiandClaude Opus 5 b305e8b154 tests: compile the five CAD test files that were never wired
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>
2026-08-12 18:05:02 +02:00