* 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
The Design view drew sinking outlines by looking up its bodies in the
plate's model, reading past the end of an object's volumes once a body
was committed. The Design canvas no longer draws sinking outlines.
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
The Windows builds of OrcaSlicer#16019 failed in DesignSketchTool.cpp:
windows.h defines near (and far) as empty macros, so
`Vec2d near = m_op_anchor;` reads as `Vec2d = m_op_anchor;` under
clang-cl. The variable is now called closest.
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
Reported: once extruded, a part is hard to read; the lighting says
little about its shape. Both lights of the object shaders sit near the
camera, so the sides of a part come out in almost the same tone, and
nothing marks where one face ends and the next begins.
- The phong shader gains a studio lighting model, chosen by a new
lighting_model uniform: a sky/ground hemisphere in world space (up
faces cool and bright, down faces warm and dark), a key light from the
upper left and a weak fill from the right, a plastic-like highlight,
and a darker base with a faint sheen toward the silhouette so curved
faces read as round. GLCanvas3D::set_studio_lighting() makes a canvas
draw its objects with it whatever the realistic-view preferences; only
the Design canvas turns it on. Every canvas sets the uniform on each
use, 0 for the slicer's, so they render as before.
- Every B-rep edge of a body is drawn as a thin dark line over it, depth
tested and pulled a few pixels toward the eye so it wins against the
faces meeting at it and hides behind the faces in front. The seam of a
closed surface and degenerate edges are left out
(GeometryEngine::display_edges). Edges are sampled once per shape and
kept across recomputes that leave a body unchanged; bodies faded by
body focus get fainter edges, and a dress-up previewing its result
alone hides them with the bodies.
Tests: display_edges gives a box its 12 edges at their lengths, a
cylinder its two round rims without the seam, a cone its base rim
without the seam or the apex.
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 sketch drawn on the rig extruded to walls with no caps. Every joint of
its loop met, so the loop analysis called it closed and MakeFace
accepted it, but the loop crossed itself: an arc left the top line's end
heading back over it and crossed it again 2.5 mm on. A second arc left a
0.28 mm line tangent to it but the other way, a cusp. The prism of that
face is an invalid solid, and it was shipped as a body.
- SketchEngine::wires_to_face checks the face it builds and, when OCCT
calls it invalid, fails with "the profile crosses or folds back on
itself, so it does not bound one region". The extrude reports that
instead of producing the broken body.
- sketch_loop_defect() judges a closed loop of lines and arcs exactly:
any contact between two of its entities away from the joints they
share, or a joint where the curve turns straight back (a cusp; OCCT
still builds that one, but it is never what was meant). It returns the
point.
- The sketch uses it on every region: the loop is tinted red, the point
gets a marker, and the status line says what the red means the first
time one appears. The MCP loop report lists the defects and no longer
calls such a profile buildable.
Tests: the rig's profile, with each defect and with both, from a
recording of the real entities. The analysis names the cusp at its joint
and the crossing on the top line, in either traversal order, and
passes ordinary tangent and collinear joints. The extrude refuses every
crossing variant with the reason, and the same arcs swept the other way
round extrude to a valid solid.
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
Twelve defects found by the 2D design pass.
- ONE erase path repairs the dimensions' cached constraint indices. Removing a
constraint renumbered m_constraints and left every later DimAnnot.con pointing
one slot short, and set_dimension_value checked only the range, not the slot's
identity — so editing a dimension's value could overwrite an unrelated
constraint. All five mutators (badge delete, fillet/chamfer, Move/Rotate/Scale,
the two drop paths) now route through erase_constraints().
- apply_dimension validates BEFORE moving or recording. A value outside a case's
threshold moved nothing and then recorded the constraint anyway, handing the
solver a number it could never satisfy; the socket guarded against this, the
tool did not.
- A Distance and its zero case are one dimension slot: typing 0 and then 5 used
to leave a Coincident AND a Distance on the same operands.
- A dimension whose constraint the solver rejected is named: the label renders in
the refusal colour and the commit says the sketch is over-constrained, instead
of showing a number the geometry does not have.
- Six silent refusals now speak: fillet/chamfer on a non-corner and on an
overrunning radius, offset on an ellipse or spline (the kernel's own reason),
a rejected array binding ladder, a dimension pick on an unsupported entity.
- set_tool commits a ready transform instead of dropping it, the rule the ready
edit-op already followed.
- Constraint releases are reported, and roles_of is one function again (the two
copies had already diverged on EllipseArc).
- Labels no longer collide: every label is its own centred ImGui window at an
anchor whose offsets are multiples of the text height, so on a feature smaller
than one text height a line's Length and Angle labels landed on the same spot.
draw_text, the one function all of them pass through, now pushes a colliding
label clear of the ones already drawn this frame.
Verified: build and LTO link on behemoth (exit 0, new binary); on the rig,
badge-delete took constraints 3->2 with dof 9->10 and the geometry untouched;
the draw-then-edit chain committed a typed 70 to exactly 70.0 mm; and a 5.4 mm
selected line renders "5.4 mm" and "21.8°" as separate readable labels.
NOT yet exercised: the stale-index corruption itself (needs a middle delete with
a labelled dimension after it), the poison-value guard through the field, the
transform commit, and the red refusal label.
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
Third measured step, and the last one I will take without a second pair of eyes.
set_as_dirty() BEFORE Refresh(): GLCanvas3D's paint handler returns without rendering when the
canvas is not marked dirty, so the previous commit's bare Refresh() posted paint events that drew
nothing and the frames stopped anyway. With both halves the pump sustains, and the trace shows the
field holding the keyboard frame after frame:
[UX] frame want_text=1 want_kb=1 active=1 buf=158.74
[UX] frame want_text=1 want_kb=1 active=1 buf=158.74 (repeating)
STILL OPEN, and now narrowed to one question: buf never changes. ImGui owns the keyboard and our
InputText is the active item, so what is missing is upstream of ImGui — the characters are not
reaching io.AddInputCharacter at all. The next thing to MEASURE (not to change) is whether
wxEVT_CHAR arrives at the GL canvas in the Design tab: DesignPanel's wxEVT_CHAR_HOOK Skips digits
while inline_busy(), but Skip only helps if the focused widget is the canvas, and nothing has yet
proved that it is at the moment the keys are sent.
Three attempts have now gone into this one point. Per the standing rule that is where solo
iteration stops.
Deployed binary restored to 00d6c191dc (md5 0eeb9a58cef5) — nothing from this branch is installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
Measured, not reasoned. A per-frame trace of the ImGui state is what finally named the mechanism,
and it is a deadlock, not a focus problem:
[UX] frame want_text=0 want_kb=0 active=0 buf=158.74 <- frame 1: the widget is not active yet
[UX] frame want_text=0 want_kb=0 active=1 buf=158.74 <- frame 2: now it is
(nothing further) <- and the canvas stops
This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a frame,
from the active item; GLCanvas3D::on_char only calls render() when update_key_data() says ImGui
wants it. So: no frames -> WantTextInput never turns on -> no render on a keystroke -> still no
frames. The characters sit in ImGui's input queue and the field is exactly as deaf as the window
it replaced, for a completely different reason.
request_frame breaks the circle, and the same trace says so:
[UX] frame want_text=1 want_kb=1 active=1
That is the first time in this file's history that the value field has owned the keyboard without
asking a window manager for it.
STILL OPEN: the typed characters do not reach the buffer (buf stays at the prefill) and the frames
stop after nine. The pump is the suspect — on software GL request_repaint() calls m_canvas->render()
SYNCHRONOUSLY, so this asks for a render from inside a render; it needs to schedule one instead.
That is the next thing to measure, not to guess.
The deployed binary on behemoth is restored to 00d6c191dc (md5 0eeb9a58cef5), byte-identical to
the last good build. Nothing from this branch is installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA