wxSpinCtrlDouble takes the mouse wheel whenever the pointer is over it. A card
taller than the panel therefore could not be scrolled past without silently
incrementing whatever field the cursor happened to be over — measured on the
rig: eight notches over the Coord Sys card turned an X hint from 1,00 into 6,00
while the panel did not move at all. The same gesture over an Extrude distance,
a Hole diameter or a mate Offset is a silent model change made by someone who
believed they were navigating, and nothing on screen reports it.
Every spin in this panel comes from one factory, so the guard goes there: an
unfocused spin hands the wheel to its parent, and the scrolled cards panel gets
it. A spin the user has deliberately focused still takes the wheel, which is the
one case where editing is what was meant.
Verified on the rig with an Extrude card: eight notches over an unfocused
Distance leave it at 10,00; clicking into it first and scrolling takes it to
13,00.
A mate between two connectors committed cleanly, recomputed without error, and
moved nothing. apply_mate was reached and computed a translation of exactly
(0, 0, 0).
The connectors were the cause, not the mate. Picking a face stores
coordsys_face and coordsys_body but left the Type combo on its default,
Point (world) — and datum_frame ignores the pick entirely for that type,
resolving the connector to coordsys_point, which is (0,0,0) unless the user
typed otherwise. Two connectors built that way share one frame, so the mate
between them is an identity transform: everything reports success and the
assembly never moves.
Capturing a face or an edge now switches the type to FaceAndDirection. Picking
a face IS the choice of a face-based frame; asking for it twice, with no hint
that the second half is required, is what made every mate a silent no-op.
Verified on the rig, two bodies with a face-based connector each:
before Extrude4 [-71.6, 402.6, 156.5] .. [-8.8, 412.6, 237.3]
after Extrude4 [-31.4, -40.4, -10.0] .. [ 31.4, 40.4, -0.0]
with the mate transform now (40.19, -196.88, 402.56) instead of (0, 0, 0).
That is the first mate in this tree that assembles anything.
Not the kernel: recompute applies mates exactly as preview does, proven by an
A/B harness over all five kinds — the two paths give identical bounding boxes.
1. A consumed loop WITH HOLES was never dropped from the sketch overlay.
sync_sketch_display compares each region's entity list against what a per-loop
extrude stored, but rebuilt the candidate from the region's OWN entities while
the extrude stores the region's entities PLUS every hole's (see
selected_loop_entities). For a plate with one bore that is 4 against 5, so the
match never fired and the extruded rectangle stayed drawn on top of the solid
it had become. Adds region_entity_indices_with_holes, which returns the same
order the extrude uses, and compares against that; a matching region now drops
its holes' entities too. Hole-less regions are unaffected.
This is also the artefact that made a correct plate-with-a-bore read as a
plate with a plug in it during rig testing.
2. The Constraints card drew its header and its first row on top of each other.
The rows and the delete buttons were parented to m_form rather than m_cards,
so they were laid out in the wrong window's coordinate space and started at
the card's top edge. Re-parented; nothing else about the card changed.
3. The mate hover ghost never appeared. The highlight handler asked for a repaint
with request_repaint(), which only queues a Refresh — and a wxMenu popup runs
its own modal loop, so the paint was not serviced until the menu closed, by
which time the ghost had been dropped. Adds DesignCanvas::repaint_now(), which
flushes the paint immediately, mirroring the m_status->Update() the status line
in the same function already needed for the same reason.
Delegated to opencode (DeepSeek V4 Pro) and reviewed by diff. Fix 1 needed an
accessor on DesignSketchTool because region_loops/RegionLoop are private — that
was outside the file list it was given, and it said so rather than working around
it.
Verified on the rig: a rectangle-plus-circle sketch extrudes to a plate with a
bore and NO overlay left on top of it, and the Constraints card shows its header
clear of eight readable rows.
Leaving Sketch clears the "N degrees of freedom" line, which is right — it
describes a sketch's constraint state and means nothing in Feature mode. But
entering CONSTRAIN left it blank too, and Constrain is where the number is the
whole point: the readout is fed only by a live solve, and no solve fires merely
because the mode changed, so the line stayed empty until the user happened to
change something.
The solve callback now caches its last result and the labelling is split into
apply_dof_status(), which set_ui_mode re-applies on entry to Constrain.
Verified on the rig: a rectangle reports "4 degrees of freedom" in Sketch, and
the same line is there after K enters Constrain.
The edge and vertex tolerances were fixed at 8 and 11 px. On a face that is
barely wider than that on screen — a thin plate, or any part once you zoom out —
every point on it lies within the edge budget, so the pick alternated edge and
whole body and the FACE level could never be reached at all. That is not just
awkward: a face pick is what gives a Coord Sys its owning body, and a mate needs
one, so thin parts could not be assembled.
Both tolerances are now capped at a third of the face's shorter on-screen side,
measured from the edge samples the picker already walks. They only ever shrink,
so a face with room keeps the full budget and nothing changes for ordinary
geometry; a narrow one keeps its middle for itself.
Verified on the rig on a 16.7 mm-wide plate at three zoom levels including one
far enough out to make the face a thin sliver: the middle reports "face 5
selected" every time, while points near the rim still take the edge.
A card that asks for a face ("Click a solid FACE in the viewport") could not be
satisfied. Clicking the same sub-element twice deliberately escalates to the
whole body — right for free picking, wrong here: the user clicks the very face
the card is pointing at, the escalation turns it into a whole-body pick, and the
armed capture rejects it and leaves "(none)". Both orders failed, so a
face-based Coord Sys was reachable only by accident of ordering. That mattered
beyond the card: a mate needs a connector with an owning body, and a face or
edge pick is the only thing that sets one.
Adds DesignSketchTool::set_escalate_on_repick, off while the Plane, Axis or
CoordSys card has a pick armed and back on as soon as it is captured or
abandoned. While armed, clicking a face means "this face", which is what the
prompt already says.
Verified on the rig: arm Pick Face, click the face, and it reads "#5" on the
first click. With nothing armed the escalation still alternates whole <-> face
as before.
The mate palette called all five kinds viable on connectors created as
Point(world), which belong to no body. Clicking one built the Mate feature and
the RECOMPUTE then failed with "mate: mate_cs_b has no associated body" — the
refusal arrived one step too late, after the feature was already in the tree,
and the user is told about it by an error line rather than by the palette that
offered the thing.
mate_options now checks the same two conditions the apply path throws on: B must
have a body (B is the connector whose body moves; A is the fixed reference and
needs none), and that body must still resolve. Either way all five kinds go
non-viable with a reason, so the offer and the kernel cannot disagree.
Verified on the rig: with two Point(world) connectors, every row is now dimmed
and reads "connector B is not attached to a body — a mate moves B's body". That
also exercises the dimmed-with-a-reason presentation for the first time, which
until now had nothing to show because every kind was always viable.
Tests: a new [mate] case covering no body, a body that no longer resolves, and
the revival once B is given one. One existing case needed its setup widened
rather than its assertion weakened: "an unrecorded fingerprint does not make a
type non-viable" built two body-less connectors, so it was asserting a side
effect of the old permissiveness instead of the property it is named for. Its
connectors now have a body, leaving the missing fingerprint as the only variable.
Full [CadDocument] suite: 2458 assertions in 185 cases.
Right-clicking a document with two coordinate systems builds the mate palette
exactly as designed: a separator, a disabled "Mate: A -> B" header, then five
rows in fixed order. Hovering a row showed no ghost and left the previous status
message on screen; clicking one created nothing at all. The palette enumerated
perfectly and fired nothing, which put the whole M8 mate epic out of reach from
the UI.
The mate handlers were never invoked. show_offer_menu binds two pairs of
handlers to the SAME wxMenu: the mate pair (by mate_base, at base + 500) and the
generic verb pair. wxWidgets pushes dynamic entries to the FRONT of the handler
list, so the pair bound LAST runs FIRST for every id — and the generic pair was
last. A mate id lands outside the verb table's range, so both generic lambdas
returned early WITHOUT e.Skip(), which wx reads as "handled", and the mate
handlers behind them never saw the event.
Binds the two generic handlers to the verb id range [base, base + 499] so each
half only sees its own ids regardless of bind order.
Verified on the rig with a purpose-built assembly (two bodies 60 mm apart, a
Coord Sys on each): before, clicking Fastened produced no feature and no status
change; after, it produces a Mate feature. The verb rows keep working — the same
run created both coordinate systems through Reference > Coord Sys.
Note the mate then fails its recompute with "mate: mate_cs_b has no associated
body", because a Point(world) coordinate system belongs to no body while the
palette still advertises all five kinds as viable. That is a separate defect and
is filed; this commit is about the palette being reachable at all.
The test executables that link libslic3r_gui (which links PkgConfig::LIBAV)
have a load-time dependency on the deps-built FFmpeg shared libraries. The CI
unit-test runner only receives the tests artifact, so those libraries were
unresolvable there (Ubuntu 24.04 ships libavcodec.so.60, not .61). Copy the
libraries next to each affected test executable and give it an $ORIGIN rpath,
mirroring the Windows branch that copies DLLs next to every test executable.
orcaslicer_copy_sos now places the copies in the per-config output directory
for multi-config generators, like orcaslicer_copy_dlls does.
Co-Authored-By: Claude <noreply@anthropic.com>
Reopening a saved CAD design ended on a modal "The file does not contain any
geometry data." warning. A CAD project legitimately carries no mesh — the model
lives in the feature tree (Metadata/SnapOrca_cad.bin) until Commit to Plate — so
the warning is false for one, and it is the last thing the user sees after
opening a design they spent an hour on. It reads as "your work is gone" at the
exact moment the recipe HAS just loaded and the Design tab is about to rehydrate
it, and the main window sits disabled behind the dialog until it is dismissed.
Counts a non-empty model.cad_recipe as geometry.
Verified on the rig: save without Commit to Plate, reopen, and the Design tab
comes up with Sketch1 -> Extrude2 -> Body 1 editable, with no dialog at all.
A rectangle with a circle inside it, drawn in ONE sketch, could not be extruded
to a plate with a bore from the GUI. Five defects were in the way. Each was
found by driving the app on a headless rig and measuring the result — the code
reads correctly at every one of these points, which is why they survived.
1. Wire orientation (kernel). SketchEngine::wires_to_face added every hole as
wires[i].Reversed(), which is only right when the sketch happens to wind both
loops the same way. A circle drawn clockwise inside a counter-clockwise
rectangle came out matching the outer boundary, OCCT swept it as a SECOND
contour, and the prism was the plate with its bore filled and the disc's
volume counted twice. Measured: bbox 67.17 x 219.67 x 10 with volume
152088 mm3 against a solid box of 147542 — a body larger than its own
bounding box, which is the signature. Holes are now added as-is and
ShapeFix_Face::FixOrientation() classifies them; that is winding-independent
and is the idiom make_extrude_regions already used for imported glyphs, which
is why holed TEXT always extruded correctly while a holed SKETCH never did.
After the fix: 142996 mm3, implied bore radius 12.03 mm against the circle
drawn.
2. The live-sketch click threw the picked region away. region_at() served only
as a yes/no gate and on_face_selected() carried no argument, so Extrude fell
back to whichever loop the resolver found first — clicking the material of a
plate-with-a-hole extruded the disc. The region is now carried through, and
DesignPanel also hands it to the tool with set_loop_pick(), AFTER open_tool()
because that re-derives selection state, since extrude_uses_loop() reads
selected_loop_entities() and that lives on the tool.
3. region_at() had no hole awareness and no innermost preference: it returned
the first polygon containing the point. It now skips a region when the point
lies inside one of that region's holes, and picks the smallest containing
loop, so a click in the bore selects the disc and a click on the material
selects the plate.
4. Startup segfault. DesignCanvas::request_repaint probed the GL backend via
OpenGLManager::get_gl_info().get_renderer() before the canvas had initialised
GL — glGetString with no context current and, before init_opengl(), no loaded
function pointers. Anything that asked for a repaint while the panel was
still being built landed there, with no window and nothing in the log. It now
bails at the top on !is_initialized() and asks for a Refresh instead. Note
the crash was in the PROBE, not in render(), which already guards itself.
5. A holed sketch on a plane whose normal points -Z came out as the full box PLUS
a disc — 220274 mm3 where 163726 was due (192000 + 28274). wires_to_face took
a SketchPlane parameter it never used and let OCCT infer a surface from the
outer wire; when the inferred normal disagreed with the sketch's, the hole
classification produced no hole. Every face is now built on the sketch's own
gp_Pln.
Also in this change, from the same rig session:
- A right-click that only clears the sketch selection no longer reports itself
as consumed, so it stops suppressing the offer menu. With any geometry in a
live sketch there was no menu route left to add a second entity.
- Escape no longer discards a live sketch that holds drawn geometry; it says so
and keeps the work (live_sketch_has_work()).
- The holed-region fill is an even-odd scanline instead of a keyhole bridge, so
no corridor triangle leaks from the bore to the nearest corner.
- Cyan is reserved for the selection: an unselected region no longer wears a
shade one step off the selected one.
- The origin planes follow the mode, so pressing Sketch on a document that
already has a body offers them again instead of naming a plane you cannot see.
Tests: three [holes] cases over add_extrude_entities asserting the plate-with-bore
volume, solid and face counts on both a +Z and a -Z sketch plane, and the
by-name refusal of two disjoint regions. Full [CadDocument] suite green.
Delete a sketch on a document that still has a body and you could not start a new
one. Pressing Sketch said "click a face or a reference plane in the viewport" —
while update_reference_planes had already called clear_base_pick, because a body
existed. The instruction named something that was no longer there, and short of
finding a face to click there was no way back into sketching at all.
The planes are now offered when there is no solid yet OR while the UI is in
Sketch mode, and set_ui_mode refreshes them so they appear the moment you press
Sketch rather than at the next tree rebuild — which is not an event that pressing
Sketch causes.
Deliberately not always-on. m_dbp_active both RENDERS and picks, so leaving it
set would float three translucent planes over every finished model. Tying them to
the mode shows them exactly when they are the thing being chosen and takes them
away again on Finish.
Safe against stealing clicks: a base-plane pick is the last resort in on_mouse,
firing only on a click that hit no geometry, so solids and committed sketches
still win where they overlap.
Found on the rig by Tommaso: "if i remove a sketch, i cannot create sketches
anymore".
A rectangle with a circle inside it extruded to a plain box. Tommaso reported it
exactly right on the first attempt — "no intersection selectable, hence no plate
with hole" — and it was a causal chain, not a guess.
Two things were wrong and they compounded.
region_loops() returned N independent filled polygons with no notion of nesting,
so the only selectable things were the rectangle alone and the circle alone. The
region a user actually wants — the bounded area WITH its hole — did not exist to
be pointed at. Worse, the first polygon containing the click won, so clicking
inside the circle selected the rectangle.
And extrude_uses_loop() hands selected_loop_entities() to add_extrude_entities,
which copied only that one loop's entities into the feature. So the circle never
reached the kernel, build_sketch_face saw a single loop, and the multi-loop path
added in 5c4ced91e7 never ran. Proven from his saved project: Sketch1 held 5
entities, Extrude2 held 4, and the committed mesh was 8 vertices — a box of
260.40 x 220.91 x 10.00. Eight vertices cannot describe a bore.
A RegionLoop now carries the loops nested inside it. Containment is decided by
testing one vertex, which is sufficient because loops in a well-formed sketch do
not cross, and each loop is assigned to the SMALLEST loop containing it so a hole
belongs to the region that actually bounds it. Picking respects holes: a click in
the plate selects the plate, a click in the bore selects the disc. The selection
hands over the region's own entities plus its holes', which is what finally
reaches the kernel. The highlight lights the holes with their region, because it
has to show what will be extruded.
The status line said "Loop selected"; it now says "Region selected". What is
selected is a bounded area that may contain holes, not a single closed curve —
the old wording described the old, broken behaviour.
snaporca-txp8, and it is what makes snaporca-88v reachable from the GUI at all:
the kernel could build the holed face all along (verified on his own recipe:
2 closed loops, wires_to_face OK, area 74812.119 mm2), but nothing could ask it to.
Reviewed and compiled (RC=0). NOT exercised — the rig check is the point.
The AppImage dependency closure resolves each bundled ELF's DT_NEEDED
entries with plain ldd, which cannot resolve the deps-built FFmpeg stack
(libavcodec/libavutil/libswscale) once it is copied into the bundle:
those libs are not installed in any standard loader path and carry no
RUNPATH of their own, so ldd reports the siblings as missing and the
build aborts. Extend the loader path with the bundle directory plus the
source directories of already-bundled files (mirroring
scripts/check_appimage_libs.sh), and key the dedup set on the bundled
file path instead of the source path so dependencies resolved from the
bundle directory are not copied onto themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
Modelling in the Design tab and pressing Ctrl+S saved a project with no feature
history at all, and the app reported success. Found on the rig: a project saved
after drawing a rectangle and a circle contained twelve archive entries, none of
them Metadata/SnapOrca_cad.bin, and a 3dmodel.model with zero vertices.
plater->model().cad_recipe was assigned in exactly one place — on_commit(),
immediately after load_mesh_object. The 3MF exporter was never at fault: it
faithfully wrote whatever the Model held, and on a save that had not gone through
Commit to Plate that string had never been set. The recipe reached the Model only
as a side effect of a different user action.
It now tracks the document instead. sync_recipe_to_model() is called after a
successful recompute, after tree edits (deletes, reorders and suppressions bypass
recompute_guarded), and from on_commit, which delegates rather than repeating the
rule. Only on success — a failed recompute leaves the document mid-edit, and
persisting that would save a model the user never had. An empty document still
clears it, so a non-CAD project carries no stale recipe.
Doing it here rather than in the save path is deliberate: Ctrl+S, Save As,
autosave and crash recovery all read model.cad_recipe, so keeping it current
after each change makes every one of them correct at once, instead of teaching
each save path to ask the Design tab. Commit to Plate means "send this to the
slicer" — making saving depend on it was the bug, not the cure.
Cost is one serialization per recompute, tens of KB against an OCCT rebuild that
has just run.
Why nothing caught it: the kernel round-trip tests serialize a CadDocument
directly, and the 3MF tests exercise the exporter with a recipe already present.
Neither can observe that the GUI never populates it, and every save in testing
happened to follow a Commit to Plate.
snaporca-vjk5. Reviewed and compiled (RC=0); persistence NOT yet confirmed on the
rig — that check is the reason the issue stays open.
wxMediaCtrl_OnSize referenced wxMediaCtrl2's private m_gtk_video_window,
which does not compile on Linux/GTK. Move the resizing into
wxMediaCtrl2::DoSetSize where the member is in scope.
This script builds only libslic3r_tests, which links libslic3r and no GUI code —
but cmake still processed the whole if(SLIC3R_GUI) block and every find_package
inside it, so the kernel suite silently depended on the GUI's dependency set.
That came due the moment upstream added wxInspector as a REQUIRED find_package:
the orcacad-deps image predates it, so configure died pointing at
src/CMakeLists.txt:92 with nothing about the kernel having changed. Turning the
block off is not a workaround for that one dependency — it is the suite finally
declaring what it actually needs, so the next GUI-side dependency added upstream
cannot break it either.
Surfaced by taking SoftFever's merge of main into the PR branch.
He merged upstream main into the PR branch himself on 2026-08-13. Taking it into
the local branch rather than force-pushing over it: the fork copy is what PR
#15238 shows, and discarding a maintainer's merge to make my own push
fast-forward would be both rude and a loss of 130 upstream commits.
Brings the branch far closer to main than the 2026-07-24 merge-base the PR body
describes, which is most of what snaporca-36u9 was filed for.
Every version bump so far has permanently orphaned every project saved before
it. deserialize_recipe refused anything that was not exactly the current
version, and with no migration path v2 and v3 projects are unopenable today —
the 3MF still carries the mesh, so the user gets a frozen solid and no feature
history, which is the whole point of the subsystem silently absent.
The cause was the shape of the data, not the gate. save/load is one flat
symmetric list of ~90 fields with no framing, so a reader has no way to know
where a feature ends unless it agrees on every field.
Each feature is now written as its own cereal stream behind a length prefix, and
the same few lines handle both directions of mismatch. Older file, newer build:
the sub-stream ends early, the read throws, and the fields already assigned are
kept while the rest default — cereal assigns sequentially, so a mid-list throw
leaves the earlier fields set, and that is what makes this work. Newer file,
older build: the sub-stream holds more bytes than the reader knows; it reads what
it knows and stops, and the outer stream is untouched because the length prefix
was consumed in full. A field a project predates is not a corrupt project, so
neither case is an error.
v4 keeps its own pre-framing flat path and opens exactly as before —
cad_recipe_v4.bin is untouched and now serves as the witness for that. v2 and v3
stay refused, by name: their field lists no longer exist in this code. This fixes
the future, not the past, and the comment says so rather than implying otherwise.
From here a new field only needs appending to save/load — no bump, no orphaned
projects. That removes the cost that had blocked snaporca-44m and snaporca-dgv.
The helix round-trip test was reading the blob back flat, reaching into the
format instead of through it; framing necessarily breaks that, so it now goes
through deserialize_recipe, which is a stronger assertion than it made before.
Every field check it carried is unchanged.
Tests: four new [CadDocument][recipe] cases, including the one the change exists
for — a deliberately truncated feature blob must LOAD, keeping what it could read.
Suite 177 -> 181 cases, 2366 -> 2411 assertions.
snaporca-2txy.