The bodies now always draw over the preview on every face a feature leaves unchanged, for
all features. Since a hole's cut sits inside the body, the Hole preview now hides the
bodies and shows the result alone, as Fillet/Chamfer and Draft do.
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced
Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes.
clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer.
* Remove Unused Project Includes From Files With Platform-Specific Code
A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block.
* Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass
The platform-file pass treated pchheader.hpp as an ordinary header and
emptied it, and left GUI_Preview.hpp and 14 other files relying on
headers they no longer reached directly.
* Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call
* Include Headers That Files Reached Through Ones the Cleanup Removed
* Drop Includes Duplicated by the Cleanup or by Main's Own Additions
* Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
Commit to Plate now sends all visible bodies to Prepare as one object with a
part per body, so their relative placement survives. A dropdown beside the
button switches to Commit to Plate (as bodies), the previous one-object-per-body
behaviour, and the choice is remembered.
Selections are drawn as opaque faces in the selection colour with a cased outline instead of a
translucent tint over the body, so they read on a body of any colour. Selecting a Feature tree
row lights the faces that feature made rather than its whole body, which also makes fillet and
chamfer rows highlight again.
Each Feature tree row now carries Edit, Show/hide and Delete, and each
Bodies row carries Move, Show/hide and Delete. These act on that row
instead of on the selection. The eye shows a closed eye when the item
is hidden. Every clickable icon in the sidebar now highlights on hover.
Rename and Color now come first in the right-click menu, as items of
their own instead of inside Modify. This applies wherever they appear,
whether opened from the viewport or from a Bodies row.
Clicking an already-selected row no longer starts a rename; use F2,
the row's right-click menu, or the Rename command.
After undo, redo, New Design, a project load, a delete or a reorder, a sketch
profile picked beforehand stayed selected even though it was gone. The
right-click menu then offered "Sketch profile" over an empty design with Create
greyed out. Those operations now clear the selection.
The Confirm button no longer stays greyed in a sketch session after a card whose
preview was invalid has been closed.
Delete and reorder now wait while a sketch or constrain session, the Text dialog,
an Insert placement or the move gizmo is open, instead of editing whichever
feature took its place in the list.
The sidebar can move to either side, float, be resized, and collapse with
the canvas button or Shift+Tab. Its layout is remembered separately from
Prepare's, starts where Prepare's sidebar is, and View > Reset Window
Layout resets both tabs.
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy
Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.
* Ignore minilzo's Config Headers in clang-tidy
lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.
* Add Missing Includes Across the Remaining Sources and Tests
Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.
* Make the GUI and Test Headers Compile on Their Own
Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.
* Keep Windows and nanosvg Setup Ahead of the Added Includes
OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.
* Add the GUI Includes the First Pass Missed
Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.
* Keep the Added Test Includes Below the NOMINMAX Guard
test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
* Add Missing Includes Across src/slic3r/GUI
Every GUI source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, plus one hand edit making CalibrationPanel.hpp self-contained.
* Drop the OS-Specific Includes Added Outside Their Platform Guards
GLib, GTK, D-Bus and POSIX headers are only used inside platform #if blocks, which already include them. Added unconditionally at the top of the file they broke the Windows build.
* Add the clang-tidy Configuration That Generated These Includes
Only misc-include-cleaner's missing-include check, with the headers it must never suggest: per-platform, internal and OS-specific ones that would break other platforms or are not meant to be included directly.
* Match Windows Paths in the clang-tidy Ignore List
Header paths use backslashes on Windows, so every / in a pattern is now [/\\]. The Windows SDK headers are ignored alongside the other OS-specific ones, and the list is one pattern per line. Suggested by @raistlin7447 from a Windows clang-cl run.
Felix14-v2's review of OrcaSlicer#16019 found the tab behaving as a world of its own.
- Icons: the design_* glyphs were drawn in a fixed light grey, made for the dark ribbon, and the
toolbar re-tinted some of them by rebuilding the bitmap from a wxImage, which drops the HiDPI
scale factor Orca sets on Windows: at 150 % the icons came out half again too large for buttons
that were sized in raw pixels, overlapping and clipped. The glyphs now use Orca's sidebar icon
grey (#949494), which the icon cache maps per theme, nothing is re-tinted, toolbar glyphs drawn
for Prepare's light toolbar use their "_dark" twin, and every size is in DIP.
- Theme: the chrome colours were read once, at construction, and nothing in the tab answered a
theme switch, so switching left light surfaces and unreadable text in a dark tab and the other
way round. The colours are now {light, dark} token pairs; MainFrame::on_sys_color_changed
reaches DesignPanel::on_sys_color_changed, which moves every token colour onto the other
theme's, runs the app's dark pass and re-rasterises the icons. Card borders are StateColors,
resolved at paint time.
- Scale: MainFrame::on_dpi_changed reaches DesignPanel::msw_rescale, which re-rasterises every
icon (buttons, flyout rows, card headers, the tree's image list, now sized from its bitmaps)
and re-measures the Orca widgets.
- Mouse: the canvas no longer forces middle-drag to orbit and right-drag to pan; it reads the
drag actions in Preferences > Control like Prepare. Left-drag is shared with picking, so the
whole-body rubber band takes Shift+left-drag while left-drag is given to the camera.
- FPS counter: ImGui's display size is shared and only refreshed when a canvas sees its own
size change; the Design canvas now re-announces its size when the tab is entered (and the
editor canvas when it is left), as Plater does between Prepare and Preview.
- Viewport text: the status line and the tool readout were top-level popups over GL. A popup
does not follow its frame, so the empty-canvas hint floated over other applications, and the
readout was never taken down with the tab. Both are drawn by the canvas in the tool's ImGui
pass now, with the theme's overlay style.
- Dialogs: messages use MessageDialog/RichMessageDialog; Add/Edit Variable is one Orca dialog
with Name and Expression fields instead of two native text prompts; the Text dialog uses
TextInput, ComboBox and CheckBox (its height is a TextInput: SpinInput is integer-only), and
enumerates the installed fonts once per session. The ribbon's Confirm/Cancel, the reference
pick buttons and the expression buttons are Orca Buttons; the variable actions are icon
buttons like the other cards'.
- Undo: the tab's own Undo/Redo buttons are gone. The top bar's Undo/Redo drive the Design
history while the tab is shown, greyed to what an undo would do, as Ctrl+Z and Edit already
did.
- The first build of the tab logs how long each phase took ("Design tab build: ..."): it is
under a second here but was reported at about fifteen on Windows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
main moved the CAD docs into docs/HLSD/design-tab.md and the offer
generator and atlas into scripts/CAD/ (#15803).
- The four docs this branch had edited are deleted as on main. What the
branch changes about the design goes into the HLSD doc: the right-click
is judged by drift over the whole press, with no time budget; a Text
feature stores its outlines as well as its string, font and height; a
new section on how the Design canvas renders bodies (studio lighting
through the shared phong shader, B-rep edge ribbons).
- gen_offer_table.py keeps both main's atlas validation and this
branch's L() markers on user-facing strings.
- The Text verb's hint is now changed in scripts/CAD/tool_atlas.json as
well, so `gen_offer_table.py --check` passes.
- DesignPanel.cpp keeps the DesignTextDialog include and main's new
path in the offer comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Reported: the Revolve axis could only be the sketch plane's X or Y axis
through its origin, so a half-profile drawn beside a centerline, the
usual way, could not be revolved about that centerline.
- CadFeature::revolve_axis_entity names a Line of the profile sketch to
revolve about (a construction centerline, or an edge of the profile
itself); -1 keeps revolve_axis. Appended at the end of the framed
recipe, so existing projects load and rebuild unchanged. Revolve and
Surface Revolve resolve their axis in one place (revolve_axis_of); an
index that no longer names a line fails with a reason.
- SketchEngine::make_revolve takes the world axis. A profile with points
on both sides of it is refused with "the profile crosses the revolve
axis"; MakeRevol failed there with no reason.
- The Axis list of both cards reads Plane X, Plane Y, then every line of
the sketch, named as the constraint list names them (Centerline E4,
Line E3). A fresh revolve preselects the sketch's centerline when it
has exactly one. The gizmo turns about the chosen axis and draws it
dashed; construction lines no longer pull its centre.
Tests: a rectangle beside a construction centerline revolves into the
tube of the expected volume along that line; about its own edge, into a
cylinder; an axis through it is refused with the reason; a stale axis
index fails with a reason; the axis survives save and load. The
truncated-recipe test accounts for the new tail field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
The context menu still said the text outline is added to the sketch as
editable lines, which is what it did before Text became its own
feature. Only SVG goes into the open sketch now; the comments on that
path say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Reported on the rig: Text showed no preview where it would go, its
dialog was pinned over the middle of the window (GNOME attaches a modal
dialog to its parent and it cannot be moved), and once confirmed the
text could not be edited and did not appear in the feature tree. Inside
an open sketch it became loose lines of that sketch.
- Text is always a feature, "Text N" in the tree. A new text goes on the
plane of the open sketch (committed first when it holds anything,
closed when it is empty), else centred on the picked face, else on the
reference plane.
- The dialog is modeless and opens at the top right of the window. The
feature is created at the first character and redrawn on every change,
so the text appears in the view where it will be as it is typed. Enter
inserts it, then the usual move/scale gizmo and Confirm; Esc, Cancel or
closing the dialog takes it out again (undo to the checkpoint taken
when it appeared).
- CadFeature keeps text_string, text_font (the WxFontUtils descriptor)
and text_height, appended at the end of the framed recipe. Editing a
Text feature reopens the dialog with them and redraws the outline in
place, keeping its placement. The outlines are still saved, so the
project opens the same on a machine without that font.
- The MCP control refuses writes while the Text dialog is open.
Tests: the text parameters survive a save and load along with the
outline; the truncated-recipe test accounts for the new tail fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Offset worked on the single entity under the pointer. On an outline made
of many short entities (a text glyph, an imported shape) that is a
segment a few tenths of a millimetre long, and the starting distance was
a tenth of that: the ghost was too small to see, and a typed distance
moved one invisible segment. It read as "Offset does nothing, no
preview".
- The pick takes the chain the entity belongs to (connected_loop, same
construction state), highlights all of it, and offsets it as one
outline through offset_entities, which already joins and trims chains
at their seams.
- The starting distance is a twentieth of the outline's size, so the
preview is visible at once.
- The arrow reads its side off the ghost, since the engine may walk the
picked entity backwards in the chain; it keeps pointing at the offset
copy, flipped for a negative distance.
- A single entity still gets its Parallel/Concentric constraint. A chain's
offset is placed as geometry, since its joined entities no longer map
one to one onto the originals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
A solid edge could only be picked one at a time, and Fillet/Chamfer took
either that one edge or a whole face group. Rounding three chosen edges
meant three features, whose edge ids each resolve against a body the
previous one had already changed.
- Shift+click (or Ctrl+click) on an edge of the body already picked adds
it to the selection, or removes it; the same modifiers that extend a
sketch selection. The whole set is highlighted. A plain click replaces
it, as before.
- Fillet/Chamfer dresses every picked edge in ONE feature at one size,
all ids resolved against the same body. The card says "3 edges", the
status line and the offer header name the count.
- CadFeature gains dressup_edges, appended at the end of the framed
recipe, so existing projects load and rebuild unchanged. dressup_edge
keeps the first edge, so an older build opening a newer project still
dresses that edge instead of falling back to the face group.
- The MCP fillet/chamfer verbs take `edge` as one id or an array.
Tests: a fillet on the four picked top edges equals the Top face group
exactly; the list survives save/load; two opposite chamfers remove
exactly twice one; a missing id fails with a reason. The truncated-
recipe test accounts for the new tail field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Drawing a line or a rounded rectangle crashed the Linux AppImage: the
constraint list labels were formatted from narrow literals holding "—",
"·" and "°". wx converts a bare char* with the current locale, and the
AppImage's AppRun sets LC_ALL=C, so the conversion failed, the format
string came out NULL and wxString::Format dereferenced it
(wxFormatConverterBase<wchar_t>::Convert, from constraint_label via
rebuild_constraint_list). A build started under a UTF-8 locale never
showed it.
These literals, and the "…" of the interference report, now go through
wxString::FromUTF8, as the panel's other non-ASCII literals already do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
- Text opens a dialog instead of a bare text entry: any installed font
(bold, italic), the height in mm, and a live outline of exactly what
will be inserted with its size. Enter inserts, Esc cancels; the last
font and height are remembered. text_to_regions gains an overload
taking a loaded font.
- The offer menu opens with a greyed title naming what the rows act on
("Flat face 4 of Body 2", "Sketch line", "Nothing selected").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
- Offer table: user-facing strings carry the L() marker so xgettext
extracts them; DesignOffer.hpp, DesignSketchTool.cpp and
SketchInlineEditor.cpp are listed in localization/i18n/list.txt.
- Offer: model-mode Constrain sits in the same row as the sketch one;
Interference is wired; Rib shows its R key; a verb that accepts the
selection but is blocked by the document stays greyed with its reason
instead of vanishing from the submenu; one refusal wording per verb.
- Extrude infers Join when the profile touches a solid (new
CadDocument::body_touching_sketch) and on face push/pull; New body in
free space. Revolve/Sweep/Loft/Boolean use the same result words.
- Interference and volume/area reports go to the status line in mm3/mm2
instead of modal dialogs; Delete Body no longer asks (it is undoable).
- New feature names match the card header ("Extrude 3"), translated;
"Coordinate system", "Angle (°)", center/color spelling, translated
face and length readouts, slot hints say width.
- CAD gizmos in Prepare are selectable only with the CAD feature on; the
sketch auto-close setting is stored per design.
- Docs: confirm/cancel rules, enabling the feature and MCP in
design_tab.md; drift-only right-click in interaction-model.md; the
portability note rewritten to describe the integration as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Keyboard and mouse
- Esc drops what is pending (picks, a dimension's first point, an edit-op or transform) and
never applies it; Enter applies a ready edit-op/transform, ends a polyline/spline chain,
ends an armed tool, and confirms a feature card exactly when its ✓ is enabled.
- Right-click only abandons the gesture in progress; with nothing pending it opens the offer
in every tool (Trim, edit-ops, transforms, Dimension, Constrain, TransformArt, move gizmo).
Clicking empty space no longer commits. The offer needs no timing, only a still press.
- Delete removes only an explicit selection. Undo/redo inside a sketch go through the same
route as the buttons (whole shapes, with redo); Edit > Undo follows the shown tab.
- The canvas no longer handles Delete/Esc/Ctrl+Z itself (Backspace in a value field deleted
the geometry it measured); F is in the panel's key map.
- Value fields: a refused value keeps the field open with the reason; click outside and Tab
commit; an untouched field commits the exact value; any decimal separator is accepted;
lengths are always mm; validation is the same for every editor.
- Snapping: the marker shows only where the click will actually snap; pick tolerances are
one set of pixel budgets (Constrain picks within reach; no mm floor on labels).
Messages and consistency
- set_status(kind, text) gives every status line its own colour and glyph; kernel errors are
translated into sentences and formatted, not concatenated; sketch refusals go to the status
line instead of the per-frame HUD that erased them.
- Hints describe the gestures that now work; Dimension shows its second step; Constrain uses
the sketch palette (red means conflict only); the straight slot's value is its width.
- Hole/Thread/Project keep the user's pick or refuse up front; circular pattern opens with its
own preview; thread fields use the nominal diameter and the ISO internal depth.
Integration
- MCP loads the project's recipe before touching the document, refuses to mutate it while the
tab is busy, never runs a request that already timed out, only replaces a socket at its
path, caps line length and removes the socket at exit; not started in the G-code viewer.
- Hiding a feature keeps later body references on their bodies; a design keeps its modeling
origin across printer changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI
scroll areas.
Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm.
This makes Preferences and other dialogs scroll consistently across
Windows DPI settings.
Rebases the in-canvas work onto cad-mainline and finishes it. The field is drawn
by ImGui inside the GL canvas instead of being a borderless top-level wxFrame.
WHY THE FLOATING FRAME COULD NOT BE FIXED. Whether a borderless top-level may hold
the keyboard is the window manager's decision, and it differs per desktop: openbox
grants it, mutter refuses it, macOS denies key status outright. Seven workarounds
fought that and one cost a macOS regression. Drawn inside the canvas there is no
second top-level for anyone to refuse, so the question is never asked. The field is
fed exactly like every other ImGui widget in the app — GLCanvas3D::on_char ->
ImGuiWrapper::update_key_data -> io.AddInputCharacter.
MEASURED, on behemoth: the click-edit ladder holds 28 checks — Line, Rectangle,
Circle, Slot, Polygon, Ellipse, Arc, and click-to-edit on a placed dimension label
— typing with NO click into the field first, committed == typed != prefill every
time, and 27 [UX] imgui_char lines showing the characters arriving.
WHAT WAS ACTUALLY WRONG. Not the field. The belief that "characters never reach the
ImGui InputText" came from the harness: the ladder was delivering keys with
`xdotool type --window` (XSendEvent), which GTK discards, so no build of any kind
could have received them. The new probe in ImGuiWrapper::update_key_data — the one
place ImGui is ever handed a character — is what separated that from a real defect,
and it stays, because a canvas-side probe provably cannot answer the question:
GLCanvas3D::on_char is bound later than any constructor-time probe, wx runs handlers
in reverse bind order, and on_char returns without Skip(), so such a probe is silent
whether or not the key arrived. A day was lost reading that silence as evidence.
Also drops DesignPanel's content-based forwarder and DesignCanvas::inline_type_char.
They were the right rule for a field that could not be focused; with the field
inside the canvas there is nothing to forward, and keeping them would have masked
whether the normal path works.
STILL UNVERIFIED: behaviour under mutter itself. Neither focus-stealing-prevention
WM available here survives long enough to judge — metacity SEGVs ~20s in and xfwm4
dies with BadWindow on SetInputFocus, both before the sketch opens and both
unrelated to this field. The design's claim is structural rather than measured: no
second top-level means no focus to refuse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
focus= in [KEYTRACE] was never a class name. GetClassName() returns const wxChar* — wchar_t* in
this build — and the line cast that to const char* and printed it with %s, so it emitted the first
byte and stopped at the padding NUL. "wxGLCanvas" came out as "w". So did "wxWindow". Every focus
reading taken from this instrument for a whole day of diagnosis was a single character, and the
one question it existed to answer — WHICH widget has the keyboard — was the one it could not
answer. Printed through wxString now, and it says focus=wxGLCanvas: the canvas does hold wx focus
while the field is open, which removes the focus hypothesis for good.
Also here, and HONESTLY LABELLED AS INCONCLUSIVE: a wxEVT_CHAR probe on the canvas. It logged
nothing (cc=0), and the tempting reading is "the characters never reach the canvas". That reading
is not available, because the probe is bound in the DesignCanvas constructor BEFORE
GLCanvas3D::bind_event_handlers(), and wx runs the most recently bound handler first —
GLCanvas3D::on_char returns without Skip() exactly when ImGui consumes a character, which is
precisely the case under test. A silent probe is therefore consistent with ImGui consuming the
keys correctly AND with them never arriving. It measures nothing. Rebind it after
bind_event_handlers(), or instrument update_key_data itself, before believing anything about it.
Writing this down rather than acting on it: I came within one commit of "fixing" a mechanism I had
inferred from an instrument that could not see it, which is the same mistake as the focus= field
above and the same mistake that cost a whole session in September.
Deployed binary restored to 00d6c191dc (md5 0eeb9a58cef5).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
The reported defect: sketch dimension labels are "not editable" — you draw a
rectangle, its Width field opens, you type, and the as-drawn number is committed
instead. It affects every sketch tool, not just the rounded rectangle.
WHAT THIS ADDS
1. The arbiter (DesignPanel CHAR_HOOK -> DesignCanvas::inline_type_char ->
SketchInlineEditor::type_char). Routes a key by what it IS, not by who the
window manager focused: digits, sign, decimal separator and Backspace/Delete
go to the open value field, Enter/Tab commit, letters stay tool shortcuts.
This is FreeCAD Sketcher's rule (DrawSketchKeyboardManager::
detectKeyboardEventHandlingMode), and the reason its sketcher behaves the same
on every desktop: it never asks who has focus.
2. The [UX] trace (SNAPORCA_UXTRACE) in SketchInlineEditor: open/commit/refused/
cancel, with the prefill and what the control actually held at Enter. It did
not exist — the ladder below was written against a surface no build emitted,
so it could only ever report "nothing opened". typed == prefill on a commit is
the defect's signature and nothing else makes it visible.
3. A draw-then-edit trace in DesignSketchTool: four early returns can swallow the
value-field chain and from outside they are indistinguishable.
4. scripts/CAD/check-gui-click-edit.py — types WITHOUT clicking the field, as a
person does, across Line/Rectangle/Circle/Slot/Polygon/Ellipse/Arc plus label
click-to-edit, and asserts committed == typed != prefill.
5. scripts/CAD/focus-loop.sh — sync/build/assert on behemoth. NOT the orcacad-gui
rig: its image pins deps 216 non-CAD files behind cad-mainline, so today's CAD
sources cannot build there without a deps rebuild.
WHAT IS PROVEN, AND WHAT IS NOT
Green under openbox: 28 checks, every tool, committed == typed != prefill.
But openbox CANNOT adjudicate this bug and the ladder says so in place. There the
field always wins the keyboard, so the same ladder also passes against a binary
with the arbiter compiled out — measured twice. Two ways of removing the keyboard
were tried and both are recorded as dead ends: XSetInputFocus loses to the field's
own re-focus CallAfter, and XSendEvent (xdotool --window) is dropped by GTK, which
made every run red regardless of the code.
Under metacity — same focus-stealing-prevention lineage as the user's mutter — the
mechanism appears in the WM's own log:
Buggy client sent a _NET_ACTIVE_WINDOW message with a timestamp of 0
That is the activation being refused, which is exactly the reported symptom.
present_toplevel() already asks for a server timestamp, so a path is still falling
through to frame->Raise(), which sends time 0. That is the next thing to fix, and
it is tracked; the arbiter alone does not close it. metacity also aborts on this
window (frames.c:1239), so the gate needs a WM that survives before it can return
a verdict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Two reports, three defects, all three measured on the running app rather than reasoned about.
"Keyboard focus in drawing tool is broken so that now they are slow and cumbersome." After a
dimensioned entity the bottom-right readout chip — 119x31, borderless, a wxFrame — held the X
input focus. Pressing r produced NO [KEYTRACE] line at all: the key never reached the panel's
CHAR_HOOK. One bare canvas click moved focus back to the main window and the identical key armed
the tool. So every shortcut was dead after every dimension, and the way to get the keyboard back
was to click somewhere harmless. That is the whole of "slow and cumbersome".
Its sibling, the status chip, is a wxPopupWindow for exactly this reason and carries a comment
warning against turning it back into a frame. The readout was left a frame on the premise that
"it appears mid-gesture and the next input is the mouse" — which the measurement falsifies: the
chip keeps the last value on screen after the gesture ends, and a frame that has the focus does
not give it back. It is now a popup too, with the placement and the iconise/deactivate lifecycle
its sibling already needed, because an override-redirect window would otherwise sit on the bare
desktop when the app is minimised.
"Planes hide the bed." Literally true, twice over. The reference planes were half-extent 0.6 *
the bed's larger side — a square 1.2x the plate — and all three are drawn with depth testing
off, so they painted over the plate grid from edge to edge. 0.3 puts them inside the bed, which
is also the Onshape look the size was reaching for: a modest square at the origin, not a
tablecloth.
And the other half was mine. 3f52166e32 muted the bed for the duration of a sketch, on the
argument that a plate grid and a sketch grid are the same visual language. The argument is right
and the call was wrong, because there IS no sketch grid to take over. Pick XY, arm Line, and the
viewport was an empty grey field: no bed, no grid, no origin, nothing to judge a length or a
direction against. The plate grid was carrying the ground reference for the whole tab. The banner
already says where you are; taking the floor away as well only made the sketch harder to draw.
The Bed checkbox is the one thing that governs the bed, in every mode.
Verified on behemoth :10 with the rebuilt binary: focus after a dimension chain is the main
window, r arms Rectangle with no click in between, and the plate grid is under the sketch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Three cues, because one is missed. A teal banner across the top of the viewport names the session
("Editing: Sketch N") and where its exits are; the printer bed is muted for the duration, since a
plate grid and a sketch grid are the same visual language and reading one as the other is how a
sketch gets drawn against the wrong reference; and N looks straight down the plane normal at the
current zoom, with the plane's own y axis as up, because no hand-orbit lands exactly square and a
sketch read at an angle is one whose right angles do not look like right angles.
The banner is an INDICATOR. Finish and Cancel stay on the single ribbon action bar — the tab had
three competing confirm surfaces once and that is not being reopened for a strip of colour. It
sits above the canvas rather than floating inside it: a child window over a wxGLCanvas is a native
window on GTK with no reliable stacking over GL, and being unmissable beats being clever.
The bed checkbox stays the stored preference and is restored on leaving the sketch; ticking it
mid-sketch still shows the bed, because that is a deliberate act and this is only a default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Two presses used to discard a live sketch. The key was answered in four places that could not
see each other — the inline value field, a sketch branch, a feature-card branch, and the canvas
— so a press aimed at one fell through to the next, and request_exit() carried a fourth layer
that deliberately let the SECOND consecutive press through to cancel_sketch(). The warning it
showed first did not help: the two presses are never one decision, the first is aimed at a field
or a tool and the second at whatever was underneath it.
The stack is now explicit. CadLevel (DesignInteraction.hpp) is four levels deep, the enum value
IS the LIFO depth, and cad_escape_level() is a constexpr function over a POD of four booleans —
so the ordering that is the entire contract is checked by static_assert at compile time, with no
window, GL context or event loop. DesignPanel::escape() acts on the one level escape_level()
names and on no other, and every Esc in the tab routes through it.
The destructive layer is gone from request_exit() itself rather than guarded at its callers, so
the guarantee cannot be re-opened by adding a route: a session holding geometry is left only
through Finish (keep) or Cancel (discard). Cancel now asks before discarding — it used to refuse
and tell the user to press the button they had just pressed, which meant a drawn sketch could be
kept but never thrown away.
Right-click also stops rewarding navigation with a menu: the offer needs BOTH budgets, released
within 200 ms and moved no more than 3 px, and the raycast uses the press position, so the menu
describes what was pointed at rather than where the camera stopped. Two budgets because drift
alone still popped a menu at the end of a slow, careful orbit.
docs/ux/interaction-model.md carries the state machine, the routing and the transition table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
The rows, their ✗ buttons and the click-to-highlight were all built against
m_doc.features[m_constrain_feat].entity_constraints — a COMMITTED feature. A live sketch
has no committed feature, so the card was hidden for the whole session and the list it
would have shown was empty by construction. Every constraint applied while drawing was
nameless: the badge said one existed, nothing said which.
rebuild_constraint_list now picks its source by scope. live_constraint_scope() is the same
discriminator apply_constraint already used to route to apply_live_constraint — both
Constrain modes set m_active, so is_sketching() alone would claim the live scope while the
committed manager is open. delete_constraint and highlight_constraint_entities branch on
it too, and the card shows in Sketch mode as well as Constrain.
Keeping the rows in step needed a signal that did not exist: on_solve_state fires on every
frame of a drag, so rebuilding from it would rebuild the list continuously. The tool now
fires on_constraints_changed only when the constraint SET changes — one added by
try_add_constraints, one removed by remove_constraint (the indexed form the badge click and
the ✗ row now share).
The rebuild is deferred through CallAfter. One of its callers is the ✗ button's own click
handler, and rebuild_constraint_list destroys those buttons: deleting the window whose
handler is still on the stack is a use-after-free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
A glyph reads as decoration until something says otherwise, so the badges shipped
last commit were discoverable only by accident. Two places now say it, chosen because
they are where the eye already is:
- the moment of applying, which is the one the user is watching ("Applied constraint ·
its badge is on the sketch — click the badge to remove it"). The hint line could not
carry this alone: it only refreshes when the (mode, step, picks) tuple changes, and
applying a constraint changes none of them.
- the Select-mode hint line, appended only while the live sketch actually holds a
constraint, so it never advertises a badge that is not on screen.
Also fixes what the previous commit got wrong: remove_constraint_near solved through
solve_sketch_entities directly, which relaxes the geometry but leaves m_dof and the
per-entity conflict flags untouched and never fires on_solve_state. Deleting a
constraint therefore left the DoF readout describing the system as it was BEFORE the
deletion, and any red over-constrained tint stranded on screen. It goes through
resolve_live() now, the same path every other live edit uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Two reports, one root: the sketch tab could not show what was selected.
The bed grid landed last commit, and selection was painted pure white — a freshly
drawn line is auto-selected by the creation tool, so the first thing a new line did
was disappear into the grid. Selection now wears design_selection_color(), the same
cyan a picked solid already wears. White is kept for the hover handle alone.
That invisibility is also why "I apply Parallel and NOTHING HAPPENS": drawing two
lines leaves exactly ONE selected (the last), Parallel needs two, so the planner
correctly refused — but the status text named only the requirement, never the current
pick, which reads as a dead button. It now reports how many are selected and how to
pick the second.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Five defects from ten minutes of real use, and the mode split behind the worst
of them. One commit because the changes overlap in the same functions; the
pieces are separable in the diff, not in the file.
CONSTRAINING NO LONGER NEEDS A COMMITTED SKETCH. A constraint could only be
applied by committing the sketch, selecting it in the feature tree, pressing the
padlock, and only then picking. While drawing, the CONSTRAIN toolbar was not even
on screen (set_ui_mode showed it in UiMode::Constrain alone) and apply_constraint
answered "Press Constrain on a sketch first" -- in a status line nobody looks at.
Draw two lines, press Parallel, get nothing: that is what a user reported as "the
UX is a mess", and they were right.
apply_constraint now takes the live session first, reading the picks from the
selection model the sketch tool already had (click, ctrl-click to extend,
double-click for the loop) and applying through try_add_constraints, which
already did append -> solve -> keep-or-rollback. The committed Constrain path
stays for editing an old sketch; it is no longer the only way in. The twenty
constraint buttons now show in Sketch mode as well.
The discriminator is is_sketching() && !is_constraining() &&
!is_constraining_entities(). Both begin_constrain and begin_constrain_entities
set m_active, so is_sketching() alone is true DURING a constrain session and the
new path would hijack the old one -- compiling perfectly and failing in
behaviour.
ONE PLANNER, NOT TWO. A second caller meant duplicating the logic that decides
whether a constraint is legal, which roles it binds and whether it needs a typed
value. That duplication is how today's Coincident bug survived: fixed in one
branch, alive in the next one down. plan_entity_constraint() now lives in the
kernel -- pure, no wx, no translation -- and both UI paths call it. DesignPanel
loses 304 lines and gains 155.
Being in the kernel makes it TESTABLE. The Parallel defect existed because a
constraint type met an entity type nobody had tried, and the only instrument was
a 13-minute GUI ladder. 19 new kernel cases cover the matrix: 264 -> 283 cases,
7648 -> 7867 assertions.
Parallel, Perpendicular and EqualLength gain the two-line guard they never had.
On non-lines they used to emit a def the solver silently dropped -- the sketch
reported itself constrained when it was not, the same class as Horizontal on a
Point. EqualLength on two rounds still promotes to EqualRadius first.
Symmetric is planned completely, including its axis pick: the plan carries a
VECTOR of defs because Symmetric on two lines is two constraints (P0/P0 and
P1/P1). A single def would have half-applied it -- one end pinned, one free,
looking correct until something moves.
A PLACED POINT SURVIVES THE COMMIT. Type::Point was created correctly and never
drawn once committed: both renderers skip it, correctly, since entity_polyline
gives a point nothing. What was missing is the vertex-marker path the live
session already used. rung_point passed throughout because it asserts the
document, and the point was always in the document -- the pixels lied.
ESC STOPS EATING AN UNSAVED SKETCH. The third press reached cancel_sketch(),
clearing m_entities with no warning and nothing to undo. live_sketch_has_work()
existed and was never consulted. The exit layer refuses once when there is work
and lets a second consecutive Esc through; the refusal re-arms on a button press,
never on mouse motion, or Esc could never exit while the hand moves.
TWO NEW RUNGS. D10 drives Parallel through the committed path -- it passes on
the PRE-fix binary, which is how we know the user's failure was the mode and not
the constraint. D11 is the acceptance for the collapse: draw, pick both, press
Parallel, no commit and no padlock. Ladder 126 -> 135 properties, all holding.
CON_BTN_SKETCH is measured, not derived: in Sketch mode the group renders after
the sketch toolbar, so the first button is at 677, not 449. Pitch 42, twenty
buttons, read off a screenshot. Deriving it by offset is how that table drifted
the last time.
Known limit, commented at the call site: a constraint added to a LIVE sketch is
not on the document undo stack, so Ctrl+Z will not take it back until the sketch
is committed.
snaporca-itp4, snaporca-oyhx, snaporca-l2vm
Port of snaporca da8d011b87; parity holds (DesignPanel.cpp still exactly 32 divergent
lines). Verified independently on this fork's own rig: full ladder 126/126 against
BuildID 96c697a3, built from this tree.
Reviewing the DistanceX/Y fix for OTHER members of its class found three more live defects
on the constrain toolbar. All four share one root: a branch assumes every picked entity has
two endpoints, and the solver's refusal to resolve a role it cannot find is silent.
COINCIDENT had the identical closest-pair walk over {P0,p0},{P1,p1}. For two Points the
phantom (0,0) pair sits at distance 0, which is the smallest distance there is, so it
ALWAYS won: ptOf(Point,P1) -> 0, ref_ok fails (SketchSolver.cpp:185), constraint dropped.
Not sometimes -- every press.
HORIZONTAL/VERTICAL hardcoded ra=P0, rb=P1 with no type check. With a Point picked the
constraint is dropped by the same mechanism but still STORED: constraints goes 0 -> 1 after
the commit and nothing moves, so the Constraints list shows a dimension that can never do
anything. Worse than refusing -- the panel claims the sketch is constrained when it is not.
ANGLE computed p1-p0 on whatever was picked. On a circle that is (0,0)-centre, so two
circles pre-filled the field with the angle between their centre POSITION VECTORS (178.83
deg for two on the x axis), and accepting it emits SLVS_C_ANGLE on two circle prims.
Both branches now refuse with a message. entity_ends()/closest_ends() are file-scope and
shared by Coincident and DistanceX/Y, so there is one implementation instead of two that
drift.
Two smaller findings from the same review: infer_auto_constraints' roles_of omitted
EllipseArc while heal_coincidences' identical copy has it; and set_point(Circle, Center)
wrote e.center and not e.p0, breaking the "p0 mirrors centre" invariant for the duration of
a live drag.
New rungs D8 and D9, both RED against the shipped binary and green here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
The Design tab's viewport is the fourth GLCanvas3D on the shared GL context and
the only one the plater does not own, so unbind_canvas_event_handlers() and
reset_canvas_volumes() never reached it — the macOS Command+Q and Debian cases
those calls exist for. Its frame-level handlers become members so they can be
unbound.
Port of snaporca 2951e3c60b; parity holds (17 identical, DesignPanel.cpp still exactly 32
divergent lines, so the hunk landed on the right side of the DropDown divergence).
The sketch-constraint epic added six toolbar buttons and covered all six with kernel
tests. Not one of them was ever clicked. The gesture ladder only pressed 'perpendicular'
and 'equal' -- and CON_BTN, which locates buttons by index, was silently wrong for every
entry past index 5 for the whole epic. The untested half of the toolbar was exactly the
broken half (snaporca-rqsy).
Four rungs now drive them: D4 Equal on two circles (must mean equal RADIUS, not the
equal-length no-op the epic fixed), D5 Collinear on two oblique lines, D6 a horizontal
distance, D7 symmetric about the implicit vertical axis. Full ladder 118/118 on snaporca;
this fork's rig has not been rebuilt against the change yet, so here it is reviewed,
parity-checked and NOT exercised.
D6 found a shipped defect. apply_entity_constraint enumerated {P0,p0},{P1,p1} for BOTH
entities regardless of type, but a Point's p1 is unused and reads (0,0), as does a
Circle's. The closest-pair search then picked those two phantom origins, distance 0: the
field opened pre-filled 0.00 and the solver dropped the constraint, because ptOf(Point,P1)
resolves to no handle. Nothing errored -- the dimension simply did nothing. ends_of() now
enumerates only the roles an entity actually exposes, and the pair with no point at all is
refused with a message instead of a silent no-op.
Five kernel tests, a 7/7 ladder, a review and a fork port all passed over this, because
every one of them exercises the kernel, where the geometry was always right.
Two rig faults fixed in the same pass, both of which produce a green-looking session that
tests nothing: start-headless-gui.sh never exported SNAPORCA_MCP or SNAPORCA_KEYTRACE, so
a freshly launched rig comes up healthy and every ladder dies on "Connection refused"; and
it never dismissed the "Restore" dialog a killed session leaves behind, which grabs every
synthetic click afterwards.
The value field also takes no keyboard focus from the WM -- typed digits go to the canvas
and Return commits the pre-filled number (typed 40, got 54.94). focus_field() finds it as
its own top-level window and clicks it first. The no-op tolerance is now 5e-3, the field's
own two-decimal display resolution, not 1e-6.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
Port of snaporca 5b1294de59. Parity OK: 17 files identical, 8 diverging at their
expected counts.
Every industrial sketcher gives you the origin and the axes as references. Here
the origin was only a SNAP target and the axes did not exist, so Symmetric needed
a third picked ENTITY as its mirror axis: symmetry about the sketch's vertical
axis first required drawing a construction line.
Every constraint reference resolves through four lambdas in the solver
(valid/ptOf/primOf/coordOf), so teaching those about three negative sentinel
indices makes the origin and both axes available to EVERY constraint type at
once. No new SketchEntity type, no serialization change; -1 still means "unset".
The references live in G_FIXED and add no degrees of freedom, which a test
asserts via the reported DoF.
SymmetricAboutY / SymmetricAboutX are two buttons that need no third pick and no
construction line. Making the axes clickable in the viewport is deliberately left
out: that is canvas hit-testing work with its own risks.
Recorded in the tests because it will catch the next person: sys.dragged[] is
populated only during a drag, so a plain sketch_solve of an UNDER-constrained
system may move any free parameter -- solvespace runs Newton, it does not
minimise movement. PointOnLine onto an axis is one equation in two unknowns and
the point legitimately slides along it. Those tests pin the free direction
instead of asserting the other coordinate is untouched.
VERIFICATION LIMIT, as with the previous two commits: this fork's kernel suite
still cannot run (find_package(assimp) fails at configure, snaporca-w80c). The
shared sources are byte-identical to snaporca's, where kernel is 2624 assertions
/ 206 cases and ALL LADDERS HELD across all seven rungs.
Port of snaporca 9fa304c77a. Parity OK: 17 files identical, 8 diverging at their
expected counts.
The everyday dimension in SolidWorks and Onshape, and this kernel had no form of
it. Distance constrains the straight-line gap; LockX/LockY pin one point's
ABSOLUTE coordinate. Neither relates two points along an axis.
DistanceX/DistanceY emit SLVS_C_PROJ_PT_DISTANCE against two unit direction
lines built in the solver's G_FIXED group, so they add no degrees of freedom.
THE DIRECTION IS SIGNED, and getting it backwards is silent. libslvs defines a
LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp) and
PROJ_PT_DISTANCE constrains (pB - pA).dot(dir) (constrainteq.cpp:234), so the
reference lines are built head-first to mean +X and +Y.
The same signedness was a real defect in the GUI: the inline editor was
pre-filled with |delta|, so when the closest endpoint pair ran right-to-left,
opening the dimension and accepting the number shown would flip the point to the
other side of its anchor. Opening a dimension and accepting its own value must
be a no-op. The refs are now ordered so the shown value is positive.
On the CAD-1000-hours corpus, dimensioning and constraining is 31.9% of all
observed CAD time -- the largest single class, 7.6x feature operations. This is
the item in the constraint epic that lands most directly on it.
VERIFICATION LIMIT, as with the previous commit: this fork's kernel suite still
cannot run (find_package(assimp) fails at configure time, snaporca-w80c). The
shared sources are byte-identical to snaporca's, where kernel is 2603 assertions
/ 200 cases and ALL LADDERS HELD across all seven rungs.
Port of snaporca 9ec6405e2d. Parity OK: 17 files identical, 8 diverging at their
expected counts (DesignPanel.cpp 32, test_slvs_constraints.cpp 3).
Measured on the CAD-1000-hours corpus: 51.5% of observed CAD time is 2D sketch
work, and dimensioning/constraining alone is 31.9% -- the largest single class.
Two constraints every industrial sketcher has were missing here.
EqualRadius fixes a dead end rather than adding a feature. Picking two circles
and pressing Equal emitted EqualLength, which maps to SLVS_C_EQUAL_LENGTH_LINES
and constrains nothing on a curve: a silent no-op with no error. Equal is now
one button with two meanings, as in Onshape and SolidWorks.
Collinear emits PARALLEL plus PT_LINE_DISTANCE=0 rather than PT_ON_LINE, whose
internal valP param this libslvs port leaves at 0, drifting an already-collinear
pair.
Both types are appended at the END of SketchConstraintType: cereal serializes it
positionally, so inserting elsewhere reinterprets every saved recipe.
VERIFICATION LIMIT, stated rather than implied: this fork's kernel suite could
NOT be run. scripts/CAD/run-kernel-tests.sh fails at CMake configure time on
find_package(assimp), before any source compiles -- a pre-existing deps gap
(snaporca-w80c), not this change. The shared sources are byte-identical to
snaporca's, where the full gate passed: kernel 2588/195 and ALL LADDERS HELD
across all seven rungs.
Also fixes two defects in this fork's scripts/CAD/run-all-checks.sh:
- `cd $(dirname $0)/..` landed in scripts/ instead of the repo root, so every
rung looked for itself under scripts/scripts/. Broken since the script moved
into scripts/CAD/; the three sibling scripts were fixed then and this was
missed, so the gate has not run since.
- C defaulted to snaporca-gui, the OTHER fork's rig container, so this fork's
gate would drive snaporca's app and report green about the wrong binary.
run-kernel-tests.sh:31 documents the identical defect being fixed once
already for the build volume; this is the third instance.