Commit Graph
31272 Commits
Author SHA1 Message Date
Tommaso Bianchi 13922a52b6 Design status: clear the DoF line on leaving sketch mode, and wrap the HUD chip
Two independent leftovers, both in the same status area.

snaporca-752: the "N degrees of freedom" line described a sketch's constraint
state and stayed on screen after Confirm, Cancel and the Escape downgrade, in
Feature mode where it means nothing — visible in every Feature-mode screenshot of
the 2026-07-27 sweep. Cleared in set_ui_mode rather than at those three exits,
because that is the one place all of them pass through and a fourth exit added
later would otherwise reintroduce it. Constrain mode keeps the readout: that is
where the number is the whole point.

snaporca-8cc: moving the status out of the panel and into the viewport HUD
removed the clipping, but not the underlying problem. The chip is a top-level
popup that Fit()s to its text, so a long sentence grew past the right edge of the
canvas and hung over the window instead of being cut off inside it — the same
silent length limit wearing a different hat. The label now wraps to the room
actually available (canvas width minus the view-cube inset), which is what makes
the earlier promise that "a sentence can be a sentence" true at 1366 as well as
at 1920.

SetLabel + Wrap + Fit are now one function called from both the text change and
the placement. Wrap() rewrites the label it is handed, so it has to follow a
fresh SetLabel every time, and the placement path runs on resize — a chip wrapped
for the old width either overhangs a narrowed canvas or wastes a widened one.
The left inset is one constant now because the wrap width and the anchor have to
agree, or the chip wraps to a width it is not then given.

snaporca-752, snaporca-8cc. Reviewed and compiled (RC=0), not exercised.
2026-08-13 08:49:18 +02:00
Tommaso Bianchi 8737ff701e i18n: drop regenerated catalogues from the PR branch
The .pot, the Italian .po and list.txt are build product: 27,314 of the added
lines in this branch were regenerated catalogues rather than code, and a reviewer
running git diff --shortstat met that number before anything else. Restored to
the merge-base so their diff is zero; they regenerate from source with
scripts/run_gettext.sh whenever the maintainers want them refreshed.

The Romanian catalogue goes with them, for a different reason: it is a complete
new translation and deserves its own PR rather than riding along inside a CAD
feature, where nobody qualified to review it would think to look.

Nothing here changes what the Design tab does. The strings are still marked for
translation in the sources; only the generated catalogues are out.
2026-08-13 08:44:44 +02:00
Tommaso Bianchi 1a6252c88c Hole/Thread re-edit: restore the face latch from the feature, not from the last pick
m_hole_on_face and m_thread_on_face are cleared only by their tool's flyout and by
their plane combobox, so after any on-face hole or thread the flag stays true for
the rest of the session. load_feature_into_dialog restored the stored plane into
the dropdown but never touched the latch, so re-editing from the feature tree
ignored the plane it had just restored: hole_plane() returned the still-latched
face plane, which may belong to a different face, a different body, or a body
since rebuilt. Silent until snaporca-200 added the "On face" row, which then read
as a confidently wrong answer rather than as nothing.

The latch is now rebuilt from the stored feature, which is the only source that
describes THIS hole. Not from the dropdown row: index_from_plane snaps an
arbitrary face plane to the nearest XY/XZ/YZ, so driving the re-edit from the row
would MOVE a hole drilled on a slanted or offset face — that was the reason the
other candidate fix was rejected.

is_base_plane() decides which of the two a stored plane is. It compares the origin
as well as the axes (a plane parallel to XY but 12 mm up snaps to row 0 and would
come back at z=0), and adds modeling_origin before comparing, because hole_plane()
and thread_plane() add it to the dropdown plane before the feature stores it — a
document with a shifted origin would otherwise mistake every dropdown hole for a
face pick. Vector norms, not isApprox, which is relative to magnitude and useless
against the zero origin.

The face's (u,v) extent is not serialized, so m_hole_has_bounds is cleared: the
gizmo's footprint clamp goes unbounded, which is honest, where another face's
bounds are not. The label says which body the face belongs to instead of a face
number the feature does not carry; "(none — uses Hole plane)" is the one thing
that is definitely false there.

snaporca-uif9. Reviewed and compiled (RC=0), not exercised.
2026-08-13 07:35:39 +02:00
Tommaso Bianchi 6b3711fb08 Mate preview: hover a mate row and see the assembly move, commit nothing (G3)
refresh_preview() listed Tool::Mate among the features that produce no solid and
cleared the ghost, with a comment saying a mate has no 3D ghost. The kernel never
agreed: preview() routes a Mate candidate through apply_mate on a throwaway copy
of the bodies, and build_candidate already filled the mate fields. That one early
return was the whole of epic gap G3.

A mate makes no NEW geometry but it MOVES a body, and the moved assembly is the
ghost worth showing. Both the Mate card and the offer's mate palette now show it:
hovering a palette row previews that kind, leaving the row drops it, and choosing
one commits. Nothing is written to the document until the click.

The committed bodies are hidden while the ghost is up — it is the whole assembly
in its post-mate pose, not an added lump, so leaving them visible would draw the
mated body twice and z-fight every other body against its own copy. Same reason
Dressup and Draft hide them.

Cleanup is after PopupMenu rather than on a close event: PopupMenu is modal, so by
then the menu is gone and any command it raised has run. A flag distinguishes a
ghost this menu put up from a preview that was already on screen.

snaporca-b4sp. Reviewed and compiled (RC=0), not exercised.
2026-08-13 07:28:21 +02:00
Noisyfox e2eebc69dd Update idle image 2026-08-13 09:12:24 +08:00
Noisyfox dc4562cbfc Fix build 2026-08-13 09:12:23 +08:00
lane.wei 7382a15b89 ENH: update some missing codes
jira: no-jira
Change-Id: Icb2da53911430ac144b0fb601637a7ad31e7e8db
(cherry picked from commit 13b4213f8a24c76c16e49daf905fa29c0f646a5a)
2026-08-13 09:12:23 +08:00
chao.zhang 0325f94de1 Fix: fix memory leak caused by ffmpeg decoding
Change-Id: I162ad4ea8d4601c1ffe17a65f292566c9dea6f0b
jira: no-jira
(cherry picked from commit eb20d03186c86b7398b97e3bae0a3c7a7b81c58c)
2026-08-13 09:12:23 +08:00
chunmao.guo ce53d34b4d ENH: wxMediaCtrl3 display video frame at pts
Change-Id: I8847236d2307101e5f2befc6477cd20b3691841c
Jira: none
(cherry picked from commit 05328da4612c11d50f6fd90e872b97f5f8f46b1d)
2026-08-13 09:12:23 +08:00
chunmao.guo 26383d5c22 FIX: TabCtrl button margin
Change-Id: If8b05a4ef9efb8b57989ee1de6543631e5a3cf90
Jira: STUDIO-8265
(cherry picked from commit 1c5e65707109ad0582b6442cf8e515344f799c27)
2026-08-13 09:12:23 +08:00
chunmao.guo 76e3da15e3 FIX: wxMediaCtrl3 zero size crash
Change-Id: I16a3f7b3afe142bb957a1740b8e8c9820c92b349
Jira: STUDIO-8522
(cherry picked from commit 8cdaea1162ccbcc0bd03ecd99346f3b9cf52cf64)
2026-08-13 09:12:23 +08:00
Bastien Nocera a9be080ebc slic3r: Fix missing BOOST_LOG_TRIVIAL declaration
src/slic3r/GUI/wxMediaCtrl3.cpp:181:23: error: ‘info’ was not declared in this scope
  181 |     BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
      |                       ^~~~
src/slic3r/GUI/wxMediaCtrl3.cpp:181:5: error: ‘BOOST_LOG_TRIVIAL’ was not declared in this scope
  181 |     BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
      |     ^~~~~~~~~~~~~~~~~

(cherry picked from commit c5c41e20ca2fc7f3b53a4c769961f73df6992008)
2026-08-13 09:12:23 +08:00
Bastien Nocera 7b5f8d00a0 slic3r: Fix missing wxPaintDC declaration
src/slic3r/GUI/wxMediaCtrl3.cpp: In member function ‘void wxMediaCtrl3::paintEvent(wxPaintEvent&)’:
src/slic3r/GUI/wxMediaCtrl3.cpp:121:5: error: ‘wxPaintDC’ was not declared in this scope; did you mean ‘wxPoint’?
  121 |     wxPaintDC dc(this);
      |     ^~~~~~~~~
      |     wxPoint

(cherry picked from commit 9ab5009235d212699f91e01d7f930f92849ed1e3)
2026-08-13 09:12:23 +08:00
Bastien Nocera 76546d89f1 slic3r: Fix missing includes in wxMediaCtrl2
src/slic3r/GUI/wxMediaCtrl2.cpp: In lambda function:
src/slic3r/GUI/wxMediaCtrl2.cpp:170:13: error: ‘wxMessageBox’ was not declared in this scope; did you mean ‘wxInfoMessageBox’?
  170 |             wxMessageBox(_L("Your system is missing H.264 codecs for GStreamer, which are required to play video.  (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Bambu Studio?)"), _L("Error"), wxOK);
      |             ^~~~~~~~~~~~
      |             wxInfoMessageBox
src/slic3r/GUI/wxMediaCtrl2.cpp: In member function ‘void wxMediaCtrl2::Load(wxURI)’:
src/slic3r/GUI/wxMediaCtrl2.cpp:179:5: error: ‘wxLog’ has not been declared
  179 |     wxLog::EnableLogging(false);
      |     ^~~~~

(cherry picked from commit 73908d38d8b1f7c8dcae92d55711bc08cbfff23c)
2026-08-13 09:12:23 +08:00
Bastien Nocera 004aea23c8 slic3r: Fix missing includes in AVVideoDecoder
In file included from src/slic3r/GUI/AVVideoDecoder.cpp:1:
src/slic3r/GUI/AVVideoDecoder.hpp:28:20: error: ‘wxImage’ has not been declared
   28 |     bool toWxImage(wxImage &image, wxSize const &size);
      |                    ^~~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:28:36: error: ‘wxSize’ has not been declared
   28 |     bool toWxImage(wxImage &image, wxSize const &size);
      |                                    ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:38:10: error: ‘vector’ in namespace ‘std’ does not name a template type
   38 |     std::vector<uint8_t> bits_;
      |          ^~~~~~
src/slic3r/GUI/AVVideoDecoder.hpp:9:1: note: ‘std::vector’ is defined in header ‘<vector>’; did you forget to ‘#include <vector>’?
    8 |     #include <libswscale/swscale.h>
  +++ |+#include <vector>
    9 | }

src/slic3r/GUI/AVVideoDecoder.cpp:145:89: error: invalid use of incomplete type ‘class wxBitmap’
  145 |     bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
      |                                                                                         ^

(cherry picked from commit 781ce14e061366da64fdc2d0d592fa35ee57e67e)
2026-08-13 09:12:22 +08:00
Bastien Nocera 3045ff7788 slic3r: Fix missing declarations in wxMediaCtrl3.h
src/slic3r/GUI/wxMediaCtrl3.h:80:10: error: ‘condition_variable’ in namespace ‘std’ does not name a type
   80 |     std::condition_variable m_cond;
      |          ^~~~~~~~~~~~~~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::condition_variable’ is defined in header ‘<condition_variable>’; did you forget to ‘#include <condition_variable>’?
   26 | #include "Printer/BambuTunnel.h"
  +++ |+#include <condition_variable>
   27 |
src/slic3r/GUI/wxMediaCtrl3.h:81:10: error: ‘thread’ in namespace ‘std’ does not name a type
   81 |     std::thread m_thread;
      |          ^~~~~~
src/slic3r/GUI/wxMediaCtrl3.h:27:1: note: ‘std::thread’ is defined in header ‘<thread>’; did you forget to ‘#include <thread>’?
   26 | #include "Printer/BambuTunnel.h"
  +++ |+#include <thread>
   27 |

In file included from src/slic3r/GUI/MediaPlayCtrl.h:17,
                 from src/slic3r/GUI/MediaPlayCtrl.cpp:1:
src/slic3r/GUI/wxMediaCtrl3.h:77:13: error: field ‘m_frame’ has incomplete type ‘wxImage’
   77 |     wxImage m_frame;
      |             ^~~~~~~

(cherry picked from commit 727a73333bd67acf5ff2b1c51ff284c2bacdb413)
2026-08-13 09:12:22 +08:00
chunmao.guo fed03193e2 FIX: reset decode buffer zero when scale width changed
Change-Id: Iaa2f99111dd5f7228b7b25e1be0a8cbdbfe982a6
Jira: STUDIO-8422
(cherry picked from commit 659ebc7d07a8f6045ba5443141b44277d7257cec)
2026-08-13 09:12:22 +08:00
chunmao.guo c3d9c27091 FIX: wxMediaCtrl3 enter Stopped state soon
Change-Id: I120e9d4b9f85599a184650d1d95fe2bec42af171
Jira: STUDIO-8280
(cherry picked from commit 7648d96305d510b9e97f22124961de5115cde830)
2026-08-13 09:12:22 +08:00
chunmao.guo 240227de79 FIX: decode video to wxImage on Linux
Change-Id: I5e332a1b0622b3dfc70ac5c4c3bfa62b3411ebdc
Jira: none
(cherry picked from commit c787ba921a31f259e8eb23fd59f178e96279caf9)
2026-08-13 09:12:22 +08:00
Mack be2be5c831 FIX: ffmpeg cmake install error
jira:nojira

Change-Id: I74cc0f7c86b5364e55cad2af2bd9a82306ee6864
(cherry picked from commit 805df79e3bb044dac29ec1c06736751ccf3675f9)
2026-08-13 09:12:22 +08:00
MackBambu 2ae0a52928 NEW:add ffmepg build Cmake
buildLinuxImage add ffmpeg so file

jira:nojira

Change-Id: I3e1be53aa58a179b8d9ae048ed7538de3ae8d111
(cherry picked from commit 2d70a1bcb6a5ba601525b08a38e7610f018fe106)
2026-08-13 09:12:22 +08:00
BBL\chuan.he d325b6b85c fix:cannot open shared object file on linux
Change-Id: Ica66500506cfe8932eac3ae0a58fb7ff30d1da9b
jira:none
(cherry picked from commit febd1aeb4d453bc96571fa5e5727e9e10046cb80)
(cherry picked from commit 5ad579f929154779abd84b01438fd235c647dbf5)
2026-08-13 09:12:22 +08:00
chunmao.guo b612cfa38b FIX: AVVideoDecoder sws_ctx_ == nullptr on zero size
Change-Id: I9698354bb1f341e276ec9780d4ef4fcd9f8a1028
Jira: STUDIO-7706
(cherry picked from commit ff622e25026a8471c39eb308cf5b115c4a9d84aa)
2026-08-13 09:12:22 +08:00
chunmao.guo 0f06620d40 FIX: wxMediaCtrl3 idle image & center pos
Change-Id: Ib9652573e31bfd6229f174c0a1388942d9d98822
Jira: STUDIO-7633
(cherry picked from commit d51247c46e26460b151de79c598d81151280e79c)
2026-08-13 09:12:21 +08:00
chunmao.guo fbc5dbcfd4 FIX: ffmpeg swscale & frame_size
Change-Id: I9f4cb8c739b726f7e5cdbe0df7ed06b2eb2154d5
Jira: STUDIO-7624
(cherry picked from commit 5a2c75d835fb437667b590a803eef148baa30875)
2026-08-13 09:12:21 +08:00
chunmao.guo 297572dc03 FIX: install ffmpeg symbolic sos
Change-Id: Ia4a45182cefcf62a7a4b4a5c89c92251609c5a68
Jira: none
(cherry picked from commit b7f8fa1efdbe0ac2cc896ca24f063f5894fe9f90)
2026-08-13 09:12:21 +08:00
chunmao.guo 8832d54b53 FIX: ffmpeg decoder memory leak
Change-Id: I997572b5730618a969959f9b24c405d80fa9f83c
Jira: STUDIO-7597
(cherry picked from commit 342cea29bd9593fa89cbb33caff58055b46ebeec)
2026-08-13 09:12:21 +08:00
chunmao.guo 7c09b0bcba FIX: reset bambu lib after restart network plugin
Change-Id: I4a3a4b7420745835ca3fa00c6edebe9d8d98cbf6
Jira: STUDIO-7571
(cherry picked from commit 28d9c6743fae80bfd40e4ee391e30d62cb16d4ab)
2026-08-13 09:12:21 +08:00
chunmao.guo 56ac17f085 NEW: reimpl wxMediaCtrl from ffmpeg
Jira: none
Change-Id: I46a47118a7649b2a50fcce8911e2888342ef25de
(cherry picked from commit d6c7f08769c8cfdbbf0e80ad280c9b3408a3c27d)
(cherry picked from commit 94d91be60bfe9bbbcdd21f85b46abc3faf126f17)
2026-08-13 09:12:21 +08:00
Noisyfox 4196c23d44 Add ffmepg dep 2026-08-13 09:12:13 +08:00
Tommaso Bianchi 8e15ad23e2 Mate palette: five types on the offer, dimmed with the reason, naming the pair
snaporca-lukg part B. The issue describes building a contextual viewport palette
with a stable icon set, non-viable options dimmed and explained rather than
hidden, and edge-aware placement. show_offer_menu() already does all three — its
dead-row branch appends a disabled row with "   —   " and a reason, and wxMenu
places itself against the screen edge. So this is not a new widget. It is one
section added to that menu, fed by mate_options().

The header row names the pair: "Mate: A → B". That is epic gap G4 — the mate card
is abstract dropdowns and never says which body moves. B is the connector on the
body that MOVES, so B is the arrow's destination; the parameter order invites the
opposite guess, which is why it is commented at the point of use.

Five rows in one loop over the kernel's result, never reordered and never
filtered. The palette addresses rows by position, so a shorter list would move
every row below it — which is the whole argument for dimming instead of hiding.

The pair comes from the Mate card's combos when that card is open, so the offer
and the card cannot disagree about what they are acting on; otherwise the first
two enabled connectors, which is defensible only because the header names them.
An offer acting on an unnamed pair would be worse than no offer.

REVIEW CATCH: the five type names arrived as an array indexed by kind, read as
_L(table[i]). That compiles and is silently untranslatable — _L is a gettext
macro and the extractor scans SOURCE for literals, so five strings would have
shipped that are never in the catalogue. These names appear nowhere else in the
tree, so that would have been their only occurrence. Now a switch of literal
_L() calls.

G3 INVESTIGATED, NOT BUILT, as specified. preview() DOES handle a Mate candidate:
it copies the committed bodies to a temporary and routes the candidate through
apply_mate on that copy, committing nothing, and build_candidate already fills
the mate fields. The only blocker to a hover preview is refresh_preview()'s
Tool::Mate early return, which clears the preview on the belief that a mate has
no ghost. Nothing in the kernel refuses it. Filed rather than built.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-lukg.
2026-08-12 23:55:24 +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 a3398c6609 MCP: let a caller find out its face/edge ids went stale
snaporca-rgbj measured the damage: four chamfers on a box remove
0.400/0.397/0.397/0.395 mm3 when each id is re-read, and
0.400/0.008/0.397/0.280 when the four ids are captured up front. The kernel is
right in both runs — the second one asks for the wrong edges. Neither errors,
because a stale id still resolves to a real edge, just not the one that was
measured.

That makes it an API problem rather than a script bug. Reading the scene once and
then issuing several operations is the natural way to drive a socket, it is what
every agent will write, and it produced silently wrong geometry with nothing
anywhere reporting it.

CadDocument::topo_generation is bumped where the bodies are replaced — the single
line in recompute() where the face and edge maps actually change, so a feature
type added later cannot forget to bump it, which a per-mutator counter would
invite. describe_scene and query_topology return it. A caller may pass it back as
"generation" on any call, and a mismatch is refused with a message that says what
to do about it.

Two deliberate choices:

OPTIONAL, not mandatory. Every existing script keeps working unchanged; passing
the generation is what buys the guarantee. Making it required would break every
caller to fix a mistake only some of them make.

CHECKED AT THE DISPATCHER, not in each handler. One site covers fillet, chamfer,
shell, draft, coordsys, thicken, cut, project, delete_face and everything added
after them. A per-handler check is a list that goes stale the first time someone
adds a method in a hurry.

Not serialized: an id means something only within the run that produced it, so
persisting the counter would promise a stability the ids themselves do not have.
No recipe version change.

describe_tools now carries an id_lifetime note, because the guard only helps a
caller who knows to ask for it.

Kernel suite 167 cases / 2277 assertions green; libslic3r_gui builds. The guard
itself is NOT exercised — it needs the socket, so it is on snaporca-bdco.
snaporca-o1l2.
2026-08-12 23:27:11 +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 488c94e957 SurfaceOffset and ThickenSurface: the arrow stands on a face, the tool still takes the sheet
These were the last tools from the charter audit with no handle at all, and the
issue filed against them offered three options, all of which changed the tool.
Reading on_add_surface_offset() dissolved the question instead.

The premise was that a distance handle needs a frame, a sheet body has no single
normal, and therefore the tool must start demanding a face. But the face was
never needed for the OPERATION — only for the ARROW. Both tools still offset or
thicken the entire sheet named in the combo. The picked face only says where to
stand the handle.

So the arrow appears whenever a face of that sheet is under selection, and its
absence costs nothing: the card alone works exactly as before. Purely additive —
no existing flow changes, and there is no new precondition for the user to learn.
That is strictly better than any of (a) anchor on the first face and be wrong on
a curved sheet, (b) sample a normal at the bbox centre and be arbitrary on a
folded one, or (c) require a face pick and change what the tool demands.

The arrow is refused when the picked face belongs to a DIFFERENT body than the
sheet in the combo. An arrow standing on one body while the tool acts on another
would name the wrong thing, which is worse than no arrow.

ThickenSurface was not on the audit's list — it is a distinct tool from Thicken,
with its own card and its own sheet-body combo, and it has exactly the same
shape. Fixing one and not the other would have left the same gap under a
different name.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-9fel.
2026-08-12 22:16:31 +02:00
Tommaso Bianchi 416e7f7321 Mate conflicts: mark the row that carries them, and name the way out
detect_mate_conflicts() has been filling m_doc.mate_conflicts on every recompute
since the kernel half landed, and nothing read it. The diagnostics existed and
were invisible — a conflicting assembly looked exactly like a working one.

The tree row is where they go, because the tree is where the user is already
looking for which feature to change. Three states, in precedence order:

  disabled  -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it
               must read as suppressed rather than keep shouting about it.
  conflict  -> warn.
  otherwise -> normal.

Selecting a marked row puts the reason on the status line — "Mate3 already
positions Body 2", the cycle, the self-mate — and names the eye as the way to
suppress it. A message that describes a problem with no action is a message that
gets ignored; the action here is already one click away on the row just selected.

Deliberately NOT a modal, and deliberately not treated as a document error. The
document still evaluates with a conflict present: the mate graph merely has more
than one answer for a body, and which one wins is the thing the user needs to
see. Blocking the loop to say so would interrupt without helping.

The dimming of non-involved bodies from the original UX proposal is still not
implemented, on purpose: under transform composition a failure mid-chain
propagates, so "not involved" is not a well-defined set, and dimming the wrong
bodies would hide the context needed to understand the conflict.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bioq.
2026-08-12 21:53:38 +02:00
Tommaso Bianchi 7311cb12cb Body-focus picking: fail open, and keep the combo and the viewport as one state
Two defects found by auditing the body-focus x-ray path, which shipped compiled
but never exercised. Neither is reachable from the happy path its test plan
walks, which is why compiling it proved nothing.

1. A STALE FOCUS KILLED THE VIEWPORT. The focus is a body INDEX held by the panel
   across recomputes, so it outlives the body it names: delete a body and the
   stored index can point past the end. body_pickable() then rejected EVERY body,
   because none of them equals an index that no longer exists — a viewport that
   silently accepts no clicks at all, with nothing on screen saying why. Out of
   range now means no restriction. Fail open, never dead.

2. THE COMBO AND THE FOCUS COULD DISAGREE. refresh_cs_body_choice() rebuilds the
   Body combo and, when the body list shrank, silently reset the selection to
   "(all)" — while the viewport stayed focused on the old index. Every other body
   kept its 25% alpha and picking stayed restricted to a body that might be gone.
   That is the exact mirror of the open_tool ordering bug this feature already
   fixed once: that one showed "Body N" over an opaque scene, this one shows
   "(all)" over a dimmed one. They are one state and are now written together.

   Guarded on CoordSys being the active tool, since it is the only card that owns
   this focus. In the edit path the function runs BEFORE open_tool with the
   previous tool still active, so the guard is false and the caller's explicit
   set_xray_focus still wins.

Also confirmed while reading, since the header asserts it: set_solid_pick() does
NOT touch m_pick_only_body, so the focus really does survive the mesh feed. That
claim now has a check behind it rather than a comment.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bgvk.
2026-08-12 21:49:08 +02:00
Tommaso Bianchi 23585382ab Mate connector: a roll mark that survives a grazing view, and a quieter warning
Two open findings from the first rig judgement of the connector glyph.

F4 — the quadrant collapses to a blob at grazing angles, which is exactly when
the roll is hardest to read. Adds a radial tick along +X extending past the disc
rim. As the disc flattens to a line the sector loses all its area, but a radial
spoke keeps its length and its direction along the one axis that still projects.

The alternative on the issue was to billboard the quadrant while the disc stayed
in-plane. Rejected, and not on taste: at true grazing the view direction lies IN
the connector's plane, so every in-plane direction projects onto the same screen
line and the roll is geometrically unrecoverable. Billboarding would not recover
it — it would face the camera and read as a definite orientation that is not the
frame's. Degrading to a direction that can still be trusted beats drawing a
confident lie. The tick is additive, so unlike billboarding it cannot make the
non-grazing case worse; it still wants judging on the rig at a true grazing view
before F4 is called closed.

F5 — roll-undefined was a loud red: the strongest colour in the viewport spent on
the least important connector, pulling the eye off the mate being made. It marks
"this one could not be derived", not an error. Muted amber says look-here without
shouting.

No tick is drawn when the roll is undefined — a tick there would assert a
direction that does not exist, which is the silent guess the hatched quadrant
exists to avoid.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-wgsc.
2026-08-12 21:46:19 +02:00
Tommaso Bianchi e6a14b39c9 Rib: the thickness gets its handle, so the whole tool is draggable
Rib's depth already reused the Extrude arrow. Its thickness could not: the arrow
points along the plane normal, and thickness is an offset either side of the rib
line, IN the plane. Different direction, different handle.

Two square handles at mid ± perp·half, plus the slab's actual footprint drawn as
a thin closed rectangle — the footprint matters more than the dots, because what
a rib thickness means is how wide that slab lands on the body, and until now
there was no way to see it before committing.

A drag on either handle sets the FULL thickness, twice the perpendicular distance
from the line, because the slab is centred on the line and the handle sits at
half. Both handles behave identically for the same reason, so they share one
colour rather than pretending to be two different actions.

A zero-length line has no direction to grow a slab perpendicular to, so the
shared rib_frame() helper returns false and render and drag both draw nothing
rather than dividing by zero. Non-Line entities clear the gizmo instead of
guessing: the kernel is line-only and a gizmo that guesses would be lying about
what Confirm will build.

Unlike the helix callback this one goes through refresh_preview(), because Rib
builds a real solid ghost that has to rebuild. The helix has none and skips it
deliberately.

Both gizmos coexist and resolve the sketch and entity the same way, so the depth
arrow and the thickness handles can never disagree about which line they are on.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-plew.
2026-08-12 21:34:59 +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
Kiss Lorand d322b1a156 Fix assembly parts omitted by height range modifiers (#15225) 2026-08-12 15:42:21 -03:00
Tommaso Bianchi e8306a6e9a Helix: draw the thing, then let the numbers be dragged
grep -i helix over the viewport code returned nothing at all. The tool was four
coupled numbers and a Confirm button — you typed radius, pitch, height and taper
blind and pressed OK to find out what you had made. So this is not only the
charter's L2 failure; the tool had no visible state whatsoever while it was open.

Adds a plane-anchored helix gizmo built on the datum-plane gizmo as its template,
being the closest existing thing: also plane-anchored, also driven by a card while
the sketch tool is inactive, also a render / hit-test / drag triad.

It draws the live curve and the axis, and puts a handle on each of the three
lengths: radius on the base circle, height at the top of the axis, pitch at the
end of the first turn — which is exactly where one pitch of rise lands, so the
handle means what it is standing on. Below one full turn the pitch handle moves
to the end of the curve rather than floating off a curve that does not exist yet.

Taper and handedness stay on the card. One is a shape modifier and the other a
flag; L2 governs numbers you can point at.

A drag reports the whole (radius, pitch, height) triple rather than one value,
because pitch and height are coupled through the turn count and writing one alone
would redraw a stale curve. The callback re-feeds the gizmo directly instead of
going through refresh_preview(), since Helix takes the produces-no-solid early
return and refresh_preview would rewrite the status line on every mouse move.

REVIEW CATCH, fixed here: the first cut read taper as a fraction of the radius
consumed over the turn count. It is an ANGLE IN DEGREES — helix_spine() builds a
Geom_ConicalSurface of half-angle taper and takes the top radius as R+H*tan(taper),
growing with the height risen. The wrong reading drew a preview that collapsed to
a point for any non-zero taper while the committed feature was perfectly fine. A
preview that lies is worse than no preview, which is what this commit replaced.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
2026-08-12 20:40:21 +02:00
Kiss Lorand ee6613a4b8 Fix stale flush matrix after enabling SEMM (#15223) 2026-08-12 15:18:00 -03:00
Tommaso Bianchi a5bb41e340 Rib: the depth is the same arrow again
Rib's depth is a distance along the sketch plane normal, so it is the Extrude
arrow for the fourth time — anchored at the midpoint of the line the rib is
built on, because a rib's line IS its profile.

This is half of Rib's L2 failure. The thickness is an in-plane offset either
side of that line and no existing gizmo draws that; it needs a handle that does
not exist yet, filed as snaporca-plew rather than left implied. One of two
numbers draggable is strictly better than neither, and saying which half is
missing is the point.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
2026-08-12 20:12:01 +02:00
Tommaso Bianchi 4ed14eb0be SurfaceExtrude and Thicken: drag the distance instead of only typing it
Both tools produce exactly one number — a distance along a known normal — and
neither had a handle for it. That is the same shape as the Extrude depth arrow,
which was already written, already draggable and already had an editable label
on the geometry. So this adds no gizmo: it points the existing one at two more
tools.

SurfaceExtrude anchors on its sketch's plane, at the profile centroid.
Thicken anchors on the picked face, and reuses the face-as-profile recipe from
the Extrude path verbatim — including the two things that path learned the hard
way: look the face up on its OWNER body rather than the whole-document compound,
and carry that body's display Move transform onto both the origin and the
normal, or the arrow draws on the bed instead of on the face.

The drag callback routes by active tool. `second` stays Extrude's alone: it is
the two-sided pair, and the other two have a single distance each.

SurfaceOffset is the third tool in this group and is deliberately NOT here. Its
target is an arbitrary sheet body, which has no single normal to anchor an arrow
on — that is a design decision, not typing, and it stays on the audit.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc.
2026-08-12 20:09:29 +02:00
Tommaso Bianchi b7397b48bd Transform: drag the body, the numbers follow
The Placement > Transform verb opened a card of spin controls — dx/dy/dz, an
axis combo, an angle — with nothing on the geometry. The 3-axis drag gizmo the
charter asks for already existed and was fully implemented (arrows, rotation
rings, click-to-type per axis), reachable only from a small icon button in the
tree card header. The prominent verb opened the form; the geometry-first
control was hidden behind an icon. That was backwards.

Transform now arms that same gizmo on the target body. The card stays as L2's
typed half: the drag writes dx/dy/dz, the axis and the angle, and the pivot is
seeded from the body's centroid so the parametric feature reproduces exactly
what was dragged.

Decomposition is exact for the interaction that matters — the gizmo's rings are
per-world-axis, so a ring drag is an axial rotation. A pose composed from two
rings is not axial and the card can only name one axis, so it reports the
dominant one rather than refusing to answer.

Three things this had to get right:

- The gizmo bakes its drag into the display transform so the body follows the
  cursor, and the feature performs the same motion parametrically. Committing
  without reverting first would move the body twice.
- tool_confirm() and tool_cancel() both tested moving_body() BEFORE the active
  tool, so with the gizmo armed Confirm would have dropped the gizmo and never
  created the feature. Both are now guarded on Tool::None.
- close_tool() is the single revert point. Esc, Cancel and switching tools all
  pass through it, so a Transform that was never committed cannot leave the body
  displaced.

Edit mode is untouched: re-seeding the gizmo from a stored feature is a separate
problem, so editing an existing Transform still gets the card alone.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-qtf4.
2026-08-12 20:04:45 +02:00
Tommaso Bianchi 3f62d4d58d Bodies: colour that survives selection, hide that toggles twice, Delete that acts
Three defects behind one report ("bodies cannot be moved or hidden/shown or
deleted, colour does not work"). They are unrelated to each other; only the
symptom was shared.

1. The Color tool wrote a per-body override that was correct end to end —
   stored on CadBody, carried across recompute (CadDocument.cpp:3381), read
   back by DesignCanvas::body_color() — and then overpainted every frame.
   m_body_selected is a DOCUMENT-WIDE flag raised whenever a non-Sketch
   feature row is selected, which is the resting state after any modelling
   operation, and while it was true every body rendered gold. An explicit
   colour now outranks the selection tint; unpainted bodies still tint, which
   is all the tint was ever for.

2. The eye toggle re-selected the body row through m_tree, using item ids that
   belong to m_parts. The row came back unselected, so the second press found
   tree_body_selection() == -1 and fell through to the feature-level branch
   instead of un-hiding. Hide worked exactly once. The sibling call in
   refresh_parts() had it right.

3. The tree card's Delete button answered a selected body row with "select the
   FEATURE that created this body" — an instruction the user cannot act on,
   because the tree does not say which feature that is. on_delete_body()
   already resolves CadBody::source_feature and confirms by name; it was
   reachable only from the right-click offer. The button now routes to it.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised — needs a session at
the machine to confirm all three in the viewport. snaporca-zjvg.
2026-08-12 19:57:14 +02:00
Tommaso BianchiandClaude Opus 5 606026a920 Home: axonometric view, fitted
DesignCanvas::set_view() and fit_view() were both written and then never called
from anywhere in the tree. The Design viewport has had no way back to a standard
view since it existed: no key, no button, nothing but orbiting by hand until the
model happens to drift into frame.

That is worse than a missing convenience. A camera left pointing along the bed
plane renders a scene that looks exactly like a failed renderer — geometry
present, nothing visible — and an hour went into blaming the software GL stack
before the real cause turned out to be two uncalled functions.

Home rather than a letter: every letter A-Z is already a Shift+letter tool
shortcut. Home is also the reset-the-view key most users arrive with. The
dispatcher needed no change, it keys on the raw wx keycode. set_view() already
does select_view + zoom_to_volumes, so this is fit and orient in one call.

Doc row added to the View toggles table in docs/design_tab.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 18:59:22 +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