Commit Graph
30135 Commits
Author SHA1 Message Date
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
Tommaso BianchiandClaude Opus 5 d584d66003 Mate conflicts: name what silently wins, don't call it over-constraint
Two enabled mates driving the same body is not an error today — the later one
just wins, and the earlier mate looks ignored with nothing said. A cycle in the
mate graph is worse: composition still produces a result, but an arbitrary,
order-dependent one.

recompute() now fills a mate_conflicts vector of (feature index, reason) before
the geometry pass, so it survives a throw further down. It catches a second mate
on the same target body, a mate positioning a body against itself, and a cycle,
via an iterative three-colour DFS over the body graph. Broken mates are skipped
silently — apply_mate() already errors on those.

Deliberately non-fatal: recompute() still returns true and error stays empty.
Deliberately not "over-constraint" — that word promises DOF analysis from a
solver this kernel does not have.

Port of snaporca ec4ffeb979. Kernel half of snaporca-bioq.
Suite: 2213 assertions / 160 cases green on this fork too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:06:17 +02:00
Tommaso BianchiandClaude Opus 5 3eec65f2bd CoordSys: pick a body first, x-ray the rest
In an assembly the face you want for a mate connector is nearly always behind
another body, and a click only ever returns the frontmost hit. Hiding the
occluder from the Parts list works but means leaving the tool mid-pick.

The CoordSys card now carries a Body chooser. Pick a body and every other one
drops to 0.25 alpha AND stops catching clicks, so the wanted face is both
visible and reachable in one gesture. "(all)" restores normal picking.

Deliberately NOT hit cycling: repeated-click cycling was removed from solid
picking as a charter L5/§10 violation (DesignSketchTool.cpp, "NO CYCLE"), and
re-introducing it here would make "click a face" a multi-click gesture again.

Mechanics: DesignSketchTool::set_pick_only_body() gates body_pickable(), which
every pick path already consults; DesignCanvas::set_xray_focus() drives both it
and the per-body alpha in reload(). The chooser stays a pick FILTER only --
coordsys_body still comes from the actual pick, so nothing in the kernel moves.

Body focus follows the CoordSys card: open_tool() reads it back from the combo
rather than clearing outright, because editing a CoordSys feature loads the card
(and its body) before open_tool runs.

snaporca-bgvk. NOT COMPILED: deps/build lacks OpenVDB so the GUI tree will not
configure here; reviewed by diff only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:45:37 +02:00
Tommaso BianchiandClaude Opus 5 8ff7ba440d CadDocument: reindex mate connectors on feature delete and reorder
mate_cs_a/mate_cs_b are feature indices. remove_feature() remapped sketch_ref
through the deletion but not the mate connectors, and move_feature() swapped
sketch_ref but not the mate connectors. Deleting or reordering any feature
ahead of a connector slid both references onto whatever features landed on
those slots.

Nothing reported it. recompute() only rejects out-of-range and non-CoordSys
targets, and a shifted index normally lands on the assembly's other CoordSys —
an assembly carries at least two by construction. So the mate resolved against
the wrong frames and moved the wrong body, silently.

Extracted a remap lambda in remove_feature() and a swap_ref lambda in
move_feature(), applied to sketch_ref and both mate connectors.

Two tests, both confirmed red before the fix. [mate] tags green here:
395 assertions / 29 cases — the first end-to-end kernel compile of this fork.

Ported from snaporca; CadDocument.cpp is byte-identical across forks again.

Refs: snaporca-kqih

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:14:10 +02:00
Tommaso Bianchi 0a2faedc32 Design: draw the mate connector, so its verse and polarity are visible
A mate connector was visible only to a program. resolve_datum_coordsys had exactly ONE consumer
in the whole tree -- McpControl.cpp, the agent socket -- so the frame every mate is built on
could not be seen at all, and the two questions a connector has to answer on sight had no
answer in the viewport: which way does Z point (the VERSE), and which of the pair is anchored
versus about to move (the POLARITY).

The glyph is Onshape's proven core plus the part nobody ships. Disc for the XY plane, one gold
quadrant for the roll -- the only in-glyph answer to "where is X", which matters because
Fastened and Slider lock the clocking -- and a Z arrow drawn on +Z ONLY, never double-headed.
Polarity is carried by the head: a filled cone travels, an open collar receives. Onshape,
Fusion, Inventor and FreeCAD all draw both ends of a mate identically, which is why "which part
moves?" is a standing complaint; nothing here invents new semantics, it just stops hiding them.

Polarity is read from the committed Mate features, not only from the open card. A connector some
mate drives must read as driven whenever it is on screen, or the glyph tells the truth only while
a dialog happens to be open. The card, when open, still wins -- that is the live intent.

Judged on the rig rather than in a mock, which changed three decisions:

- Three RGB axis arms lose to one Z arrow. Rendered side by side (SNAPORCA_GLYPH=A selects the
  Onshape-style trio), the three heads are as large as the 22 px disc, they bury the quadrant, and
  at an oblique angle they pile into a smudge -- and the trio is indistinguishable from the move
  gizmo and the bed triad, which are already RGB arrow trios in this viewport.

- Depth off floats, depth on tears. With GL_DEPTH_TEST off, connectors on faces pointing AWAY from
  the camera drew their discs over the solid, so the part looked covered in frames that were on its
  back. Turning depth on fixed that and immediately z-fought: the disc is exactly coplanar with its
  face and came out a broken dotted arc. Depth ON plus a 0.7*upp lift along Z buys both, and scaling
  the lift by upp keeps it sub-pixel instead of opening a visible gap on zoom-in.

- Foreshortening degenerates an arrow into a dot when the axis points at the camera. It now draws a
  ring instead of silently vanishing, which is what a naive projection does.

Everything is sized in screen pixels via upp = 1/zoom, like every other gizmo here: a connector is
a symbol, not a part, so it must not shrink with the model.

Research and the empirical findings are written up in DESIGN_MATE_CONNECTORS.md section 8b;
rig images in artifacts/shots/g-0*.png, vendor reference glyphs in artifacts/glyphs/.

Not fixed here, and recorded rather than papered over: the quadrant collapses to a blob at a
grazing angle, which is exactly when the roll is hardest to read (F4); roll-undefined in red makes
the least important connector the loudest thing on screen (F5); and a true grazing view, a curved
face, and overlap with the move gizmo are still untested. Also surfaced while testing and unrelated
to drawing: add_mate accepted a mate between two connectors on the SAME body and duly transformed
the body relative to itself -- a concrete instance of the missing validation already filed as G6.

Fork parity unchanged: DesignCanvas.cpp 16, DesignPanel.cpp 30, the other four files 0.
2026-08-05 09:50:09 +02:00
Tommaso Bianchi 24f4076bb5 Design: the Hole and Thread cards say which face they are holding
snaporca-200 asked which of the two models of "the card's face input" is right,
because clicking empty canvas now clears the selection (snaporca-od0) and made
them visibly disagree: Thicken / Shell / Draft read the LIVE selection and their
label reverts to "(pick a solid face)", while Hole / Thread LATCH the face they
were opened or picked on and keep it. The complaint was that Hole then drills a
face you can no longer see selected.

Taken to the rig, that turns out to be the wrong half of the story. With Hole
open and its face picked, a click on empty canvas leaves the Ø6.0 ghost and its
dimension gizmo drawn on that exact face — the card was never operating in
secret, it was showing its target the strongest way a CAD tool can. Meanwhile
Draft, whose behaviour was held up as the honest one, threw the pick away and
had to be told the face again.

So neither model replaces the other. They are different in kind: Thicken /
Shell / Draft are operations whose operand IS the selected face, and Hole /
Thread are placement tools with their own plane state that a pick merely seeds.
The latch is also the kinder of the two now that empty clicks are a deliberate
gesture — a stray one costs Thicken a pick and costs Hole nothing.

What was genuinely missing is that nothing in those two cards NAMED the latched
face, so after such a click the only words on screen were the viewport's
"Nothing selected" over a ghost about to drill. Both cards now carry an "On
face" row, the way the other three already do:

  Hole    Face 5 | (none — uses Hole plane)
  Thread  Face 1 | Edge 2 | (none — uses Thread plane)

Thread names an edge when the cylinder came from a circular rim rather than a
cylindrical face, which the code already distinguished internally and never
said out loud.

Verified on both rigs, every state driven through the GUI: face pick on open
and on live pick, survival across a click on empty canvas, and the fallback
after choosing XY/XZ/YZ from the plane dropdown. Thread's edge branch was
exercised on a revolved tube's rim, its face branch on the same tube's outer
wall.

Filed while here, surfaced by the new row rather than caused by it —
snaporca-uif9: re-editing a stored Hole/Thread from the feature tree restores
f.plane into the dropdown but never clears m_hole_on_face, so the re-edit
silently reuses the PREVIOUS card's latched face. Now visible by name instead
of invisible.

Fork parity unchanged: DesignPanel.cpp 30, DesignPanel.hpp 0.
2026-08-03 15:06:17 +02:00
Tommaso Bianchi 93395b7888 Design: escalate on the entity that was picked, and let the chip follow the window
Both found by Kimi reviewing the previous two commits, both then reproduced here
before being touched.

snaporca-97z. The re-pick escalation required m_solid_sel, m_sel_body, m_sel_face AND
m_sel_edge to all match the previous pick. That looked stricter and was wrong: the
edge branch sets only m_sel_edge and m_solid_sel, leaving m_sel_face as whichever face
the ray happened to enter through — and a shared edge is entered through a different
face depending on which side you view it from. So picking an edge and picking that
same edge again from the other side compared equal edges, unequal faces, and refused
the escalation the status line had just promised. Now the comparison is made at the
level that was picked and nothing else. The edge id is already the stable global one
from edge_index_of, so it identifies the edge without help from the face.

Reproduced on the rig without needing to orbit, since two clicks 8px apart across an
edge enter through different faces:

  pick -> sel=3 body=0 face=5 edge=3
  ray  -> body=0 face=0
  re-pick -> escalated to whole body 0
  pick -> sel=1 body=0 face=-1 edge=-1

Same edge, face 5 then face 0, escalation fires. The old condition could not.

The frame-move case. The chip is anchored at an absolute screen position, and until now
nothing told it the window had moved — only a resize, a status change or a tab switch
re-placed it. Dragging the window by its title bar left it stranded where it was,
verified on the rig by moving the frame and watching it stay put. wxEVT_MOVE on the
top-level frame, alongside the ICONIZE and ACTIVATE binds from the previous commit.

Two related cases are filed rather than bound, because the list of window-geometry
events to chase is exactly what snaporca-lcq argues should stop: a layout change that
translates the canvas without resizing it, and wxEVT_DPI_CHANGED.

Not fixed, deliberately, and recorded on snaporca-97z: clicking the same FACE but
landing within the vertex or edge tolerance resolves to a different kind and so does
not escalate — that is the "smallest thing under the cursor" rule working as
documented; and a vertex re-pick after moving the body compares stale world
coordinates.

Verified on both rigs. The cross-face edge case was exercised on orca_cad;
DesignSketchTool.cpp is byte-identical across the forks, so snaporca inherits it, and
its face-level escalation and empty-click clear were re-checked there directly.

Fork parity unchanged: DesignSketchTool.cpp 0, DesignCanvas.cpp 16.
2026-08-03 14:17:57 +02:00
Tommaso Bianchi 6d1a4078ca Design: the status chip goes away with the window, not just with the page
Found by minimising the app on the rig with a face selected: the whole screen goes
black and the chip is still drawn on the bare desktop. A wxPopupWindow is
override-redirect — the window manager does not own it — so it neither iconises with
its frame nor stacks behind other applications. IsShownOnScreen does not catch this
either: an iconised frame still counts as shown, which is why the guard added for the
tab case sails straight past it.

So the frame has to say so itself: ICONIZE and ACTIVATE, both routed through the same
show_status_hud the page change already uses. Restoring is safe — a popup cannot take
focus, so our own Show() cannot re-trigger either event — and restoring while some
other page is up still leaves the chip down, because show_status_hud(true) goes
through place_status_hud's IsShownOnScreen guard.

Verified on both rigs: chip up, minimise -> screen black and empty, restore -> chip
back with its text and the face still selected. Restore while on Prepare -> chip stays
down.

This is the fourth defect from the same root, so snaporca-lcq now asks the question
these binds keep deferring: whether the line should be canvas content, like the view
cube and the round view buttons, rather than a window that has to be told about every
way a window can stop being visible.

Fork parity unchanged: DesignCanvas.cpp 16.
2026-08-03 13:36:09 +02:00
Tommaso Bianchi 9c3ce9b45e Design: clicking empty space lets go of the selection, and the status line follows its tab
Two things a click on nothing should already have done.

snaporca-od0. A click that hit no geometry left the solid selection standing. A
rubber band swept over empty space has always cleared it (pick_bodies_in_rectangle),
and the two gestures cannot disagree about the same outcome. The visible cost was in
the escalation that landed last commit: "click the face, click away, click the face
again" arrived as the SECOND click on the same face and took the whole body, when the
click away was the user letting go of it. Now the miss clears and says so.

This gives up something real, deliberately: Thicken / Shell / Draft hold their input
face in the panel's selection, so a stray click on empty canvas with one of those
cards open hands that face back. Their handlers already write the "(pick a solid
face)" placeholder and rebuild the ghost when the selection empties, so the card SAYS
it lost the pick rather than confirming against a face the viewport has stopped
highlighting. An orbit drag never reaches this branch — it exits at the 8px budget —
so panning the view still does not deselect.

snaporca-dlj. The status line is a wxPopupWindow, which is a TOP-LEVEL window: hiding
the Design page does not hide it. Select a face, switch to Prepare, and the chip was
still there reading "selected (whole body) — right-click for what applies to it" on a
tab with no such selection and no such menu. Same cause, second symptom: a status
update arriving while the page is hidden anchored against a client size that is not
the size the page will have, and parked the chip on the tab bar. So: an
IsShownOnScreen guard in place_status_hud, show_status_hud(bool) to take it down and
bring it back with its text intact, driven from the page-changed handler.

Verified on both rigs, not by reasoning about it: face 5 selected -> click bed ->
"Nothing selected", tint gone -> click the same face -> face 5 again, NOT the body ->
click it again with no click away -> whole body, so snaporca-gem is intact. Prepare ->
chip gone; back to Design -> chip returns. KEYTRACE across the round trip shows
shift+S then R still reaching the canvas (ui_mode 0 -> 1, Rectangle armed), which is
the focus theft this popup replaced a wxFrame to avoid.

Fork parity unchanged: DesignPanel.cpp 30, DesignCanvas.cpp 16, headers and
DesignSketchTool.cpp 0. MainFrame.cpp is outside that set and was edited per fork.
2026-08-03 11:05:45 +02:00
Tommaso Bianchi c32aa3f8ba Design: clicking the same face twice takes the body, and the status line moves onto the viewport
A click could point at a face, an edge or a vertex, but never at the body those
belong to: offer_selection_kind() can only return BodySolid when all three are
clear, which a viewport click never produces. The rubber band was the only door,
and the status line said "face 5 selected" while the user believed they had taken
the body. A second click on the SAME sub-element now escalates to it (snaporca-gem).

Not the pick cycle that was removed in bc2b741ce9 -- that one was silent and three
deep, so no click had a predictable meaning. Here the status line names the next
click before you make it, and a further click just takes the face under the cursor
again, which needs no teaching. Double-click is untouched: wx sends Down/Up/DClick/Up
and only the first Up carries a pending press, so a fast double-click still zooms to
fit and picks once.

The status line itself moved to the base of the viewport. In the side panel it was
clipped at ~73 characters with no warning and no wrap -- set_status()'s Wrap() never
took effect (snaporca-8cc) -- which silently length-limited every hint in the tab; the
first version of this change lost a clause to it. m_status is kept, hidden, as the
owner of the text and its colour, and the line is drawn in a bottom-left twin of the
readout HUD where there is a whole window's width.

Three defects found driving it on the rig, none of which the build could see:

  * the HUD as a wxFrame took the WM's keyboard focus every time it was raised, and
    the canvas then received NO key events -- every sketch shortcut silently dead.
    Caught with SNAPORCA_KEYTRACE: shift+S logged a line, the following R logged
    nothing. It is a wxPopupWindow now, which cannot be focused. SetFocus() on the
    canvas does not fix it: focus was on another toplevel.
  * zero vertical padding fits the popup tighter than the font's line box and clips
    the glyphs; 6 (what the readout uses) reads as a two-line box. 3 is right.
  * "has a caller chosen a colour?" compared the label's foreground against its
    PARENT's, which differ by default, so every line counted as chosen and the
    neutral text came out the panel's dark grey -- invisible on a dark chip. Compare
    against the colour the label was created with, captured before any caller writes.

Verified on both rigs against fresh binaries: sketch -> extrude -> click face ->
click again -> whole body tinted, offer opens with the body rows live and Create /
Add material correctly greyed. Keyboard drives the whole sequence.

Filed and NOT fixed here: snaporca-od0 -- a bare-plate click does not deselect the
solid, so "click away, click back" escalates. Pre-existing; clearing there would also
drop the face the Thicken/Shell/Draft cards hold, which needs its own pass.

Refs: snaporca-gem, snaporca-8cc, snaporca-od0
2026-08-02 12:12:50 +02:00
Tommaso Bianchi 7e5994b8cb Design: right-click a body row opens the offer, and taking a body always means the same thing
The third door onto the offer, after the viewport right-click and the Menu key. A body ROW is
an unambiguous body, so the offer reports BodySolid and the body verbs act on the row you can
see highlighted — the confirmation a face pick cannot give, since pointing at a face lights the
face and never the body the verb will change. The status line has been promising exactly this
("Body N selected — right-click for what applies to it") since before any handler existed on
that list; the product was advertising a gesture that did nothing.

WHAT THE RIG CAUGHT THAT THE BUILD DID NOT. The first version hung the state normalisation off
wxEVT_TREE_SEL_CHANGED. But SelectItem() on a row that is ALREADY selected fires no selection
event, so a stale vertex from an earlier viewport pick survived — and offer_selection_kind()
tests vertex FIRST, so right-clicking the body row served the VERTEX offer while the row sat
highlighted: Fillet/chamfer/draft greyed, Mirror standing where Repeat belongs, "vertex
selected" still in the status line and the cyan marker still on screen. The happy path (fresh
row, nothing else picked) looked perfect, which is why only the deliberate stale-state sequence
exposed it. Reading the code would not have shown it — SelectItem looks like it selects.

So the normalisation is no longer a selection handler. apply_body_row() is called
UNCONDITIONALLY by both doors, because taking a body from the list means the same state change
however it was asked for. It also clears m_sel_solid_vertex, which the original handler never
did — latent while nothing opened the offer from that list, and immediately fatal once
something did.

Verified on both rigs with the failing sequence itself: pick a vertex, then right-click the
already-selected row. Fillet/chamfer/draft enabled, Repeat back in place, status reads "Body 1
selected", vertex marker gone.

Does NOT touch the feature tree. That needs new selection kinds (offer_selection_kind has no
notion of "a feature is selected") plus verbs the atlas does not contain — Suppress, Rename,
Reorder, Roll back — and is filed separately.
2026-08-02 10:31:21 +02:00
Tommaso Bianchi 6c59898ac0 Design: pointing at part of a body is pointing at the body
Tommaso: "i deleted a body using rubber band selection, but this is not intuitive as all
the ux revolves around clicking". Correct on both counts, and a correction to what I said
last round: the rubber band IS implemented and shipping (pick_bodies_in_rectangle, m_rubber,
the drag branch in on_mouse). What is unbound is whole-body picking via CLICK; I read the
comment about the click path and wrongly generalised it to the gesture as a whole.

The handlers were never the problem either. Move, Mirror, Cut, Mass and Colour all resolve
their target through selected_body_default() / m_sel_solid_body, and that is already set when
you click a FACE — level >= 1 records the body. They would have worked from a click all
along. The only thing keeping them out was the atlas gate: accepts listed body_solid and no
face kind, so offer_selection_kind() returning FacePlanar filtered the rows away. This is
therefore an atlas-only change, no handler edits.

Cut, Split, Mirror, Transform, Mass and Colour now accept face/edge/vertex as well, matching
what Delete Body already did. Edges and vertices are included deliberately, not just faces: a
click resolves to a vertex, an edge or a face depending on where inside the pixel it lands,
so accepting only faces would make Move vanish whenever you clicked near a corner — a flicker
that reads as a bug and gets reported as "sometimes it works".

NOT widened: Extrude on a face means push/pull THAT face, and Thicken consumes the face you
point at. Both have genuine face-specific meaning, so widening them would change what they
do rather than where they can be reached from.

The rubber band keeps its job — it is still the only way to take a body without also naming
one of its faces. It just stops being the only door.

Verified on both rigs from a plain face click: Transform > Move opens with Body = Extrude2
(resolved from the face pick), Modify > Edit / Delete Face / Colour / Delete Body, and
Reference > Mass.
2026-08-02 09:57:53 +02:00
Tommaso Bianchi b2654ebd8a Design: a body knows what made it, so "Delete Body" can exist
Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent
faults stacked behind that.

FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound
(DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields
Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but
no face kind, so the row was filtered out entirely — while the status line read "Body 1
face 0 selected", which actively teaches the wrong model.

SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the
FEATURE that created this body". CadBody had no link back to its maker, so the offer was
advertising a verb it could not perform — worse than the action:null rows fixed earlier this
session, because this one is ENABLED and its refusal reads like user error.

CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop,
and the rule is just "still unset?". That is sufficient because of an invariant worth
stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape
(boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the
stamp it was born with; a consumed body is erased outright, taking its stamp with it; and
the only bodies still at -1 are the ones the current feature just pushed. A feature type
added later needs no change here as long as it keeps to that invariant.

"Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by
pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the
body kinds so both can never appear for one selection. Deleting a body removes the feature
that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a
body vanishing from the viewport is not evidence of which feature went, and this is the one
action here that cannot be eyeballed.

Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler
deletes exactly one body, so a two-body selection would have silently deleted whichever was
m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild.

Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete
Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint ->
Undo -> Extrude2 and Body 1 restored.
2026-08-02 09:41:58 +02:00
Tommaso Bianchi 34eb4224a1 Design: fix a wrong issue ref in the Thicken comment
The previous commit cites snaporca-y7q, which does not exist — I wrote the ID from memory
instead of reading it back from the bug I had just filed. The real one is snaporca-kgx,
"Offer: Thicken (and peers) open with the picked face discarded". The comment is corrected
here; the commit message above it cannot be, so this note is the pointer.
2026-08-02 09:03:49 +02:00
Tommaso Bianchi 9d47280a19 Design: a card opened from a face must use, and show, that face
snaporca-y7q. Thicken's opener cleared m_sel_solid_face outright. That was right when the
only door was a toolbar button — a button carries no selection, so pressing Thicken had to
clear and ask you to point at something. The offer inverted it: the verb is now invoked ON
a face, and the same line threw away the only thing the user had said. The card opened
reading "(pick a solid face)" over an immediate "thicken: face not found" — you pointed at
the face and were told none could be found.

Keep the pick when the body combo landed on the body it came from (the index is per-body,
and selected_body_default() returns exactly that body when it is valid).

Two neighbours had the mirror-image flaw, both invisible for the same reason — the value
was right and the ghost updated, so only the label lied:
  - Thicken had NO live label update at all. Nothing outside the opener ever wrote
    m_thicken_face_label, so while the card was open you could pick face after face and it
    still read "(pick a solid face)".
  - Shell and Draft wrote theirs ONLY from the pick handler, which runs while a card is
    already open — so opened from a selection they showed the previous pick, or the
    placeholder over a face they were about to use.

So the label is now written once in open_tool(), which every door goes through. The
edit-feature path already restores m_sel_solid_face from the stored feature BEFORE calling
open_tool, so it agrees rather than fights.

Verified on the snaporca rig: face 4 of an extruded plate, offer > Add material > Thicken
now opens "Face: Face 4" with "Preview — 24 triangles" and confirms to a real Body 2. Draft
opened from a face shows "Face 3" and previews the taper. This fork is code-identical here
bar the two permitted DropDown divergences; it still owes a build of its own (snaporca-5pl).

Project keeps its clear: there "(all edges)" is a legitimate default mode rather than a
failure, so changing it would alter behaviour with no reported problem behind it.
2026-08-02 09:02:52 +02:00
Tommaso Bianchi cfc2555c3a Design: a verb's address is data, so the toolbar widget can stop existing
snaporca-7ih's remaining half. Both flyout factories registered their verbs INSIDE the
widget-building loop, so the ~40 retired tool buttons had to be constructed and then
Hide()n: skipping construction would have deleted 42 offer verbs (26 fly:<family>#<row>
+ 16 Shift+keys) while their rows still rendered and did nothing when picked.

Register first, build second. The addresses are pure data; the widget is one door onto
them, not their owner. A family absent from kBarKeep now returns before any wxWindow is
made. The keep-list stays a one-line data decision, not a structural one.

And close the class of bug for good: the constructor now verifies, once, that every verb
the atlas marks wired resolves to a real registration, logging each break and asserting in
debug. Rows that render and do nothing have shipped three times (edit_feature and sk_move
with action:null, then this) and are invisible from either side alone.

Verified on the snaporca rig by walking the offer, not by reading the code — all four
at-risk address kinds run with no widget behind them: fly:design_rect#2 drew an OBLIQUE
rectangle (the third variant, not the family's first), key:S+E opened Extrude with its
10 mm gizmo, fly:material#4 opened Thicken. Hover hints, icons and nesting intact. This
fork is code-identical here bar the two permitted DropDown divergences; it still owes a
build of its own (snaporca-5pl).

Two hints were wrong and are fixed: Cut said "Split the body with a plane", colliding with
the Split verb one row away and pointing at a card for a value the canvas already offers as
a draggable arrow; Split never said its plane comes from a picked face.

Also, because it blocked the verification and will block the next one: gui-session.sh
killed by full path while its own app_pid() matched by basename, so a differently-pathed
instance survived, held the single-instance lock, and got reported as a healthy session —
a Jul-30 binary nearly passed as this build. It now kills by basename and prints which
binary is actually on screen. Traps 6 and 7 documented.
2026-08-02 08:37:44 +02:00
Tommaso BianchiandClaude Opus 5 96816f725c Design: every offer verb has a hint, shown on hover — and the status line wraps
Mirror of snaporca 2b3e890165 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).

All 86 verbs now carry a hint: 55 extracted from the C++ tool definitions so the offer and
the armed-tool hint cannot drift, 31 written by hand. One wxEVT_MENU_HIGHLIGHT binding
shows the hovered verb's hint in the status line. The generator asserts that no wired verb
lacks one.

Also: all 200 status writes go through set_status(), which wraps instead of clipping at the
panel edge; and the empty-document hint is called from on_tab_shown() as well, since
after_tree_edit() never runs on a freshly opened tab.

Verified on the rig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 19:46:49 +02:00
Tommaso BianchiandClaude Opus 5 3037e55f44 Design: hints name the gesture that works, and an empty document says how to start
Mirror of snaporca 033347d062 (parity 30 / 16).

Retiring the toolbar made ten hints untrue: each named an action whose door had moved to
the offer, or a button no longer on the bar. They now name the gesture. An empty document
blanked the status line entirely and now says how to start.

Known and not fixed here: m_status does not wrap, so long hints clip; and the 86 offer
verbs still have no per-verb hint of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 19:31:33 +02:00
Tommaso BianchiandClaude Opus 5 811719b7aa Design: Construction goes back on the sketch bar — a mode must show its state
Mirror of snaporca b3d4cf85af (parity 30 / 16).

Hiding it with the drawing tools was wrong: Construction is a persistent MODE, not a tool —
the Bed checkbox, not the Line button. Q and the offer's Construction row kept toggling a
checkbox nobody could see, so you could not tell whether the next line would be construction
geometry.

Scoping unchanged and already correct: m_tb_sketch is shown only in UiMode::Sketch, so it
appears exactly while a sketch is open or being edited.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 18:36:18 +02:00
Tommaso BianchiandClaude Opus 5 86f1f96c50 Design: the toolbar is chrome — every tool is reached from the offer
Mirror of snaporca 809aa9df87 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).

fadd() and sadd() now gate what reaches the bar: file operations, Bed, Undo/Redo, Delete
selected, Commit to Plate, Confirm/Cancel, plus Place on Face and Section view — the last
two because they are chrome_only in the atlas and have no offer row to fall back on.

The tool buttons are still built and then hidden, deliberately: their fly: addresses and
Shift+key bindings are registered inside the widget-building loops, so not building them
would silently drop 42 verbs from the offer while they still rendered. snaporca-7ih covers
hoisting the registrations so the construction can go too.

Four separators whose groups are now empty were dropped; they rendered as stray rules.

Verified on the rig in both modes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 18:17:34 +02:00
Tommaso BianchiandClaude Opus 5 a535b0cb76 Design: the offer draws each verb's icon — set the bitmap BEFORE Append, not after
Mirror of snaporca bcab67f8ce (DesignPanel.cpp applied as a patch; parity 30 / 16, shared
files byte-identical).

tool_atlas.json now names an icon for 80 of 86 verbs, derived from the toolbar's own
definitions rather than invented, and every one of the 54 distinct names was checked to
exist in resources/images first.

The first attempt drew nothing despite a green build: wxGTK builds the GtkMenuItem inside
Append() and reads GetBitmap() there, so setting the bitmap on the returned item is a
silent no-op. append_offer_item() constructs, sets, then appends — the same order Orca's
own append_menu_item() uses.

Verified on the rig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 17:55:52 +02:00
Tommaso BianchiandClaude Opus 5 bc4fb3b680 Design: Text and SVG draw INTO the open sketch instead of beside it
Mirror of snaporca 1fb7786d9a (DesignPanel.cpp and DesignCanvas.cpp applied as patches;
parity 30 / 16, shared files byte-identical).

With a sketch open, Text/SVG outlines become ordinary Line entities via push_closed_lines()
instead of a separate Sketch feature carrying rigid imported_regions — so the letters can
be constrained, trimmed and extruded like anything drawn by hand. The buttons and offer
actions arm Select first when in Sketch mode, since begin_sketch() does not run until a
tool is armed.

add_imported_regions() calls reset_autoedit(): without it the glyph contours entered the
draw-then-edit queue and opened a Length field on the first segment, which freezes the
canvas. Caught on the rig, not by reading.

No sketch open: unchanged — a new Sketch feature, still dropped on a picked face.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 17:38:44 +02:00
Tommaso BianchiandClaude Opus 5 447c71a0d2 Design: Text and SVG join Create — they were excluded on a premise that is not true
Mirror of snaporca 6724ea27c5 (DesignPanel.cpp applied as a patch; parity 30, shared files
byte-identical).

chrome_only's rule is "acts on the DOCUMENT, not on a selection". Text and SVG both call
add_imported_sketch(), which drops the art on a picked solid face via
SketchPlane::from_face() — a selection-consuming profile creator, like Sketch. Now
sk_text / sk_svg in the sketch half's Create row, where their toolbar buttons already sit.

Verified on the rig: Create ends Point, Text, SVG.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 17:14:13 +02:00
Tommaso BianchiandClaude Opus 5 d5c5d5675e Design: a body tool acts on the body you picked, not on the first one
Mirror of snaporca e5e223a794 (DesignPanel.cpp applied as a patch; parity 30, hpp
byte-identical).

Every body combo opened on index 0, so picking a body and pressing Mirror acted on a
different solid while the card showed that other body as the target. Nine sites now read
the viewport selection; Boolean takes the picked body as target and a different one as
tool, since defaulting both to the same body is a no-op.

Verified functionally on the rig: picked the 20x20 body, mirrored, and the new body
measures 20x20 — not the 80x50 one it would have used before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 12:15:46 +02:00
Tommaso BianchiandClaude Opus 5 273cf067e8 Design: record that Shell stays in Remove — decided, not overlooked
Mirror of snaporca e2ef3018cd. Data only, and only the `why` prose in two slots — the
generated DesignOffer.hpp is byte-identical, so there is nothing to rebuild.

Ratified 2026-08-01: the offer deliberately splits the toolbar's dressup family. Shell
hollows a solid so it sits in Remove; Delete Face edits an existing solid so it sits in
Modify. Recorded in both slots' `why` so either half explains the split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 10:01:34 +02:00
Tommaso BianchiandClaude Opus 5 f6e6cd83c1 Design: the fillet row is named for the tools it actually holds
Mirror of snaporca 352cf1c259 (data only — tool_atlas.json + the regenerated
DesignOffer.hpp; both byte-identical across the forks).

"Fillet / chamfer / draft" rather than naming shell too: Shell is in Remove and Delete
Face in Modify. Only the toolbar's dressup dropdown groups all five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 09:08:21 +02:00
Tommaso BianchiandClaude Opus 5 0c2b643b0b Design: the card says which tool it is, and "Dress-up" stops being a word we use
Mirror of snaporca 3677937964 (DesignPanel.cpp applied as a patch; parity 30, shared
files byte-identical).

The card header read "Fillet 1" over a chamfer because the offer's Chamfer address opened
the tool before setting the type, and open_tool() titles the card from that combo. Choose
first, then open.

"Dress-up" removed from the offer row (-> "Fillet / chamfer"), the card field (-> "Type",
it was a label reading Dress-up whose value said Chamfer) and the toolbar tooltip.

Verified on the rig: header "Chamfer 1", field "Type: Chamfer", row "Fillet / chamfer".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 09:00:23 +02:00
Tommaso BianchiandClaude Opus 5 3c998b8a62 Design: a tool's options come from the tool, not from a card on the left
Mirror of snaporca 6d5510734b (DesignPanel.cpp applied as a patch; parity re-checked at
30 lines, shared files byte-identical).

Polygon's Sides/Circumscribed card is deleted — the choice is made in Create > Polygon,
which names the counts and the two fits, because the side count cannot be recovered after
drawing. Dress-up, Combine and Pattern were single verbs hiding several behind a combo
and now name each one in the offer. Fixes fillet and chamfer both carrying key:S+F, which
made the offer's Chamfer open a Fillet.

Built green and verified on the rig: the Dress-up card opened from Chamfer reads Chamfer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 08:48:05 +02:00
Tommaso BianchiandClaude Opus 5 d1d61ce997 Design: every sketch tool has an address in the offer, not just its family
Mirror of snaporca 0c83f59f13 (DesignPanel.cpp applied as a patch, not copied, so this
fork's 30 permitted divergent lines survive; parity re-checked at 30/16 with the shared
files byte-identical).

The sketch dropdown never registered "fly:<family>#<row>" addresses the way
feat_dropdown does for model verbs, so the offer could name a family but only ever arm
its first tool — Rectangle always gave a corner rectangle. Adds the registration, 14
atlas verbs (including the entire array family, which was absent, and rotate/scale), an
action for the sk_move row that previously did nothing when picked, and a second submenu
level so variants nest under their family instead of flattening 19 create tools.

Built green and verified on the rig: Oblique rectangle arms oblique, not corner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 08:20:47 +02:00
Tommaso BianchiandClaude Opus 5 fd1bc092d8 Design: slot Radius caption, keyboard offer, plane combo removal, mass props, docs
Mirror of snaporca ac85277bac..0e7cb3ec78 (six changes, applied as a patch to
DesignPanel.cpp rather than copied, so this fork's 30 permitted divergent lines survive
— parity re-checked afterwards: the five shared files are byte-identical, DesignCanvas.cpp
and DesignPanel.cpp differ by exactly 16 and 30 lines).

- The straight slot's inline field says Radius, which is what it sets. It stores the
  half-width and passed the typed number through unchanged, so 30 produced a 60 mm slot.
- The offer opens from the keyboard (Menu, Shift+F10), anchored on the viewport rather
  than wherever the pointer happens to be. The card hint names the new route.
- The sketch card's Plane combo is gone; the plane comes from the viewport. Also stops
  build_candidate collapsing a face plane to a base plane while editing.
- Mass properties and the dead Edit row are wired into the offer; DesignOffer.hpp is
  regenerated from tool_atlas.json, verified by re-running the generator and diffing.
- docs/rig_build_traps.md + scripts/rig-build.sh, which derives its fork identity from
  project() so it cannot be pointed at the other fork's image or volume.
- docs/design_tab.md refreshed (44 commits stale) + a PR description, with this fork's
  own merge-base and diff shape rather than snaporca's.

Built green in the deps container with the new script and verified on the rig: Menu and
Shift+F10 both open the offer at the viewport centre with the pointer parked off-canvas,
and the sketch card shows no Plane row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 06:21:08 +02:00
Tommaso BianchiandClaude Opus 5 6e9910303b Design: a sketch takes its floating chrome with it when it ends
Confirming a sketch while an inline value field was open left the field behind. The
editor is a top-level frame, so it survived the session that owned it, and inline_busy
stayed set with it — on_mouse_impl then returned true at its first branch for every
later click and the viewport was simply dead. No refusal, no message: exactly the
"click the geometry, nothing happens" the pick bugs above it were mistaken for.

finish() and cancel() now call close_session_chrome(): dismiss the open field
(keep-as-drawn, the same contract the polyline terminators already use), drop the
queue of fields behind it, and clear the corner readout — which had the same defect
for the same reason, sitting on 336.8° over a committed sketch because nothing redraws
the HUD once the tool stops.

Verified on the rig on the exact reported sequence: line on XZ, Return to accept the
length, Confirm with the Angle field still open. The field goes, the sketch commits,
and the next click reaches the pick (pick trace shows down/up consumed) and selects
Sketch1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-08-01 02:35:29 +02:00
Tommaso BianchiandClaude Opus 5 1addba6ea0 Design: editing a quote UPDATES its dimension instead of appending a rival to it
Type a new length into a sketch quote and the number changed while the geometry
sat still, with the solver dropping to "Conflicting constraints" — a broken
constraint state the user never asked for, arrived at by doing the one thing the
status line invited.

An experiment separated the two candidate causes. Same gesture, one variable:
a line committed WITH a driving length refused the edit and conflicted; a line
committed with none (Esc keeps it as drawn) accepted it and visibly shrank. So
the value was never the problem and the solver was not wrong — it was being
handed two contradicting constraints and correctly declining to choose.

The quote-edit path appended unconditionally:

    a.con = int(m_constraints.size());
    m_constraints.push_back(constraint_for(a));

Accepting the length at draw time creates Distance(P0,P1) = 64.9. Clicking the
quote later creates a SECOND Distance on the same two points asking for 30. Over
-constrained by construction. A line with no dimension yet only ever gets one
constraint, which is exactly why this looked intermittent rather than total.

upsert_constraint() finds an existing constraint with the same type and operands,
overwrites its value and returns its index; it appends only when there is none.
Operand order is ignored — a Distance from A to B is the same constraint as B to
A, and so is an Angle. Returning the index matters as much as the update: it
keeps the annotation's `con` pointing at the constraint that is actually live, so
the NEXT edit is an update too rather than reverting to appending after one good
round. upsert_dimension() applies the same rule to the visible quote, which had
been stacking labels reading different values on the same pixel, and keeps the
existing label position so a placed quote does not teleport.

set_dimension_value already did the right thing through a.con. The machinery
existed; these two call sites never consulted it.

Verified on :11 on the exact failing case — draw a line, Return to lock the
length, Confirm, double-click to re-open, click the line, type 30 into the quote:
the line shrinks, the quote reads 30.0 mm, one label not two, and the solver
stays at "3 degrees of freedom" with no conflict.

NOT included: record_dimension_constraint() has the same unconditional push_back
in all six of its branches. It belongs to the legacy Constrain mode with
different selection semantics and I could not exercise it, so it is flagged
rather than changed blind.

Refs snaporca-e1p.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-31 21:35:41 +02:00
Tommaso BianchiandClaude Opus 5 a1fdb9f217 Design: double-click a sketch stroke to edit it — the gesture belongs on the geometry
Selecting a committed sketch line lit the right tree row and then told the user
to go and press Edit in the panel. That is the side-panel dependency this tab
exists to remove, and it made "selectable" true while "editable from the
geometry" stayed false.

on_edit_feature already does the whole job — re-open the entities in the sketch
UI with handles and live quotes — and was only ever reachable from a tree row.
A double-click on a committed stroke now calls it for that feature. Double-click
on empty space still fits the view, so nothing is taken away.

The stroke hit test is now one hit_display_sketch() shared by the click and the
double-click. Two copies of "what is under the pointer" drift, and a double-click
acting on a different entity than the click before it is a miserable thing to
chase. It also reports the entity index, which the tracer prints, so a pick that
lands on the wrong stroke can be seen rather than inferred.

Verified on :11 end to end: draw an open line, commit, double-click it. The
tracer prints "double-click -> edit sketch feature 0 (entity 0)", the panel
reads "Editing sketch — drag a handle or click a quote to edit", and a click
inside the session selects the line with endpoint handles, live quotes and
"1 selected — Delete removes them".

NOT delivered by this commit, found while verifying it: typing a new value into
a length quote is accepted and displayed but the geometry does not move and the
solver drops to "Conflicting constraints". Filed separately — it lives in the
constraint layer, not in selection, and nothing here touches it.

Refs snaporca-e1p.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-31 21:10:08 +02:00
Tommaso BianchiandClaude Opus 5 c5404507c7 Design: an open sketch line can be clicked — region membership is not a licence to be pointed at
Reported from the rig: a committed sketch holding a single open line rendered on
the plate and could not be selected, so it could not be edited or deleted from
the viewport at all.

The viewport pick for committed sketches iterated region_loops(). That function
exists to find EXTRUDABLE regions and, as its own walk comment says, an open
chain "stalls" and is discarded. So for a sketch of open entities it returns an
empty list, the pick loop has nothing to iterate, and every click falls through
to bare plate. Not a tolerance problem and not a focus problem: there was no
candidate geometry to test against.

Whether a stroke bounds a closed region has nothing to do with whether the user
can point at it. The stroke test now covers every non-construction entity, and a
separate entity->region map preserves what a hit REPORTS, so a click inside a
closed loop still names that loop exactly as before. Region membership decides
the report, not whether the hit can happen.

The rest of the path was already written and simply unreachable: the panel's
handler has a region < 0 branch that selects the feature, highlights it in the
tree and says "Sketch selected — Extrude it, or Edit / Delete from the tree".
This makes existing behaviour reachable rather than adding new behaviour.

Verified on :11 with the pick tracer (SNAPORCA_PICK_TRACE=1), which is what
distinguished the two failure modes: before, the click reached the handler and
fell through to handle_solid_click ("no solid data"); after, it is consumed by
the display-sketch test and never reaches it, and the panel reads "Sketch
selected" with Sketch1 lit in the tree.

Construction geometry stays unpickable, matching region_loops' own filter. It is
the same class of bug — a construction line cannot be selected to delete it —
but including it risks construction stealing picks from real geometry, so it is
left as a separate decision.

Refs snaporca-e1p.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-31 19:55:56 +02:00
Tommaso BianchiandClaude Opus 5 9f2b2bc511 Design: sketch means a tool — the offer works inside a sketch, and the app stops
contradicting itself about the plane

Reported from the rig: pick a plane, press Sketch, and you are told to pick a
plane. The app prescribed a sequence and then refused to acknowledge that you
had followed it. Right-click did nothing, so there was no way to reach a
drawing tool except the toolbar this tab exists to retire.

act_sketch was two lines: set the mode, then print "Click a face or a reference
plane in the viewport, then a sketch tool" — unconditionally, without ever
asking whether a plane was already chosen. The plane was never lost; m_ref_plane
held it and begin_sketch captures it when the first tool is armed. The sentence
was simply false.

It now asks. sketch_plane_target() is a companion to sketch_plane_from_selection
that distinguishes "the user chose XZ" from "nothing chosen, falling back to XY"
— a distinction m_ref_plane cannot express on its own, being always a valid
index, so m_plane_picked carries it. With a target the readout names it and the
offer opens on the Create row; without one the old prompt stands, because then
it is true. The card above the status line was a local wxStaticText that nothing
could update, so it went on asking for a plane two inches from a line saying the
plane was chosen. It is a member now and the two are written together.

Right-click inside a sketch was excluded wholesale so that it could end a
polyline chain, abandon an anchor, exit a tool. That made every sketch row in
the atlas unreachable. The honest test is not which mode we are in but whether
the tool actually USED this right-click, and only the tool knows: on_mouse now
wraps on_mouse_impl and records that once, for every terminator, instead of
threading a flag through the twenty-odd sites that consume a RightDown. The
canvas read-and-clears it on the matching release.

Underneath all of it was one confusion — MODE versus SESSION — at four sites.
begin_sketch does not run until the first tool is armed, so is_sketching() is
false for exactly the interval between "press Sketch" and "pick a tool", which
is precisely when the drawing tools must be on offer. The keyboard learned this
once already (snaporca-0ud, whose comment states the rule) and I reintroduced it
in offer_selection_kind and again in show_offer_menu, where the offer built from
the FEATURE map and rendered nine rows that all refused the sketch selection.
Both now call sketch_map_applies(), so they cannot drift apart again. The
keyboard keeps its own split: its "sketching" gates undo and delete-last-entity,
which genuinely need a live session.

Verified on :11 against a fresh build. Pick XZ, press Sketch: card reads
"Drawing on XZ", status reads "Sketching on XZ — pick a tool", offer opens with
Create and Reference live and the six rows needing geometry greyed. Right-click
while idle opens the offer. Right-click as a terminator does NOT — the chain
ends, the line lands on XZ, its length field arms at 49.36 mm. That last one is
the regression the blanket exclusion was buying and the reason this shape of fix
was chosen over a mode test.

Refs snaporca-6vs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-31 19:21:42 +02:00
Tommaso BianchiandClaude Opus 5 bb403b82cf Design: left-drag sweeps a rubber band, and it takes the whole body
The pick cycle died two commits ago and left no viewport route to a whole
body at all: one click resolves vertex, edge or face, double-click is
already zoom-to-fit, and the only way to take a body was the Bodies list —
a geometry-first violation for as long as it stood. The rubber band is that
route.

Left-drag is the gesture, as asked. That button was orbit, so this canvas
now maps the mouse the way every CAD the user already knows does: left
selects, middle orbits, right pans. The change is a single flag on
GLCanvas3D set only by DesignCanvas, so Prepare and Preview keep the mouse
their users learned. Sketch mode inherits the same mapping, which is the
consistent reading — Design is one modality, not two.

Past an 8 px budget a press becomes a sweep, anchored at the ORIGINAL press
point rather than at the frame where the threshold was crossed, so the
first few pixels are not lost. Below the budget it is still a click and the
existing vertex/edge/face pick runs untouched. Sampling is the display
mesh's triangle vertices plus centroids — the same points the ray pick
tests, already in world coordinates — and the body with the most samples
inside wins, because the selection callback downstream carries one body.
Crossing semantics: touching selects. Enclosed-only for left-to-right and
crossing for right-to-left is the fuller CAD convention and is deferred,
not forgotten; with one selectable body it would have bought nothing.

Two defects fixed on the way, both found by exercising this:

Right-drag pans, and every pan ended by popping the offer over wherever the
camera stopped — the context menu arriving as the reward for moving the
view. The offer is now the release of a STATIONARY right-click, at the same
8 px budget the pick uses.

The selection handler wrote m_status twice. Only the later write ever
reached the screen, so the earlier block had been dead since it was
written, and its labels drifted out of step with the live ones unnoticed —
including a vertex fix I made this morning in the branch that never
renders. Deleted, with a note saying why, rather than left as two writers
for the next person to pick the wrong one.

Verified on :11 against a fresh build: click takes face 5; left-drag across
the body reports "selected (whole body)" with the whole solid tinted and
the camera unmoved; left-drag over empty space clears; stationary
right-click opens the offer; right-drag pans with no menu; middle-drag
orbits. Precedence re-checked after the deletion — face at 25 px from the
corner, vertex from 10 px in.

Refs snaporca-9xw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-31 18:11:59 +02:00
Tommaso Bianchi 33f97d259b Design: vertex picking — a click near a corner takes the corner
Completes the precedence Tommaso asked for: vertex, then edge, then face, all
from ONE click, all decided in screen pixels. Verified on :11 by sweeping into
the right corner of a plate — x=1250 and 1290 report "face 5 selected", x=1308,
1318 and 1323 report "vertex selected" — and the cyan marker lands exactly on
the corner in the render.

The vertex tolerance (11 px) is deliberately LARGER than the edge one (8 px). A
corner lies ON its edges, so equal radii would make vertices unreachable: every
click near one would resolve to the edge underneath. Bigger-wins-first is what
makes the smallest entity actually pickable.

Vertices come from the sampled edge polylines' endpoints rather than a separate
topology walk — every corner of a face is the end of one of its edges, so the
data was already in hand.

The highlight is a camera-facing square scaled by 1/zoom, the same trick the
edge ribbon uses, so it reads as a constant dot at any zoom. This is why vertex
picking did not ship with the previous commit: the Edge render path billboards
a ribbon and degenerates on a two-point input, and a selection you cannot see
is not a selection (L5). Better to add the primitive than to fake the feature.

DesignPanel now distinguishes level 4: a picked corner sets neither face nor
edge, because a corner is not its face, and the offer classifies it as
OfferSel::Vertex — where Plane, Axis and Coord Sys already accept it.

snaporca-9xw (rubber band still open — see the issue).
2026-07-31 12:48:15 +02:00
Tommaso Bianchi b003d20e37 Design: kill the pick cycle — one click selects what is under the cursor
Tommaso, correctly: fix selection before building on it. I had taken the
whole→face→edge click cycle as terrain and hung the tool offer off it, when §10
of the charter already listed that cycle as an L5 violation. An offer can only
ever be as truthful as the selection beneath it, so this is the foundation and
it should have come first.

NOW: one click selects the SMALLEST thing under the pointer — the edge if the
cursor is within tolerance of one, otherwise the face. No repeat clicks, no
state, no memory of what was picked before. Verified by sweeping a column of
single clicks down a plate on :11: y=800..915 all report "face 5 selected", and
y=925/935 — within a few pixels of the front edge — report "edge 3 selected".
One gesture, one deterministic result, which is what L5 asks for.

TOLERANCE IS IN SCREEN PIXELS. The old edge step compared a ray-to-segment
distance in millimetres, so the same gesture meant different things at
different zoom levels. The pointer is a screen object; its tolerance has to be
one too. 8 px, measured against the edge polyline projected through the camera.

WHAT IS NOT HERE, AND WHY IT IS NOT FAKED. Whole-body selection has no viewport
gesture in this commit. Double-click is ALREADY zoom-to-fit, bound earlier in
the same on_mouse, and this pick runs on LeftUp where LeftDClick() can never be
true — so a double-click branch here would have been dead code that reads like
a working feature. I wrote one, found it unreachable, and deleted it rather
than leave it. The body gesture is the rubber band, which is its own piece of
work; until it lands bodies are selected from the Bodies list, and the hole is
named in a comment at the site instead of being left for someone to trip over.

Six status strings that promised the cycle ("click again for a face", "click
again for an edge", "click again to reset") are gone — they described a
behaviour that no longer exists, and a hint that lies is worse than none.

Both forks build. Parity: DesignSketchTool.cpp byte-identical, DesignPanel.cpp
30 divergent lines — the invariant exactly.

snaporca-6vs.
2026-07-31 12:37:21 +02:00
Tommaso Bianchi 7114e316ea Design: the offer ships — right-click the geometry, get what applies to it
Row order is RATIFIED (charter 4.1, 2026-07-31) and this is the first working
implementation of it: right-click in the Design viewport and a vertical list
opens at the pointer with the eight families in their fixed order, the verbs
that apply live, and the ones that do not disabled IN PLACE carrying their
reason.

THE MAP EXISTS ONCE. DesignOffer.hpp is GENERATED from docs/ux/tool_atlas.json
by docs/ux/mockups/gen_offer_table.py — the same file the 113 mockups are drawn
from. A drawing and the product therefore cannot drift apart, which is the only
way row constancy survives contact with a codebase. Never hand-edit the header.

NOTHING IS RE-IMPLEMENTED. Each row routes to the code that already runs that
verb: "key:S+E" through m_keys_feature, "key:L" through m_keys_sketch,
"fly:material#4" through the feature flyout's own action, "btn:colour" through
the standalone button. The offer is a second door onto the same room, so the
toolbar, the shortcuts and the menu cannot drift into three behaviours. The 8
verbs with kernel support but no GUI path show disabled, which is honest and
matches section 10 of the charter.

Right-click only fires the offer when the canvas is IDLE. Right-click already
ends a polyline chain and finishes the move gizmo; taking those over would
break two working interactions to add a third.

Two things the running build corrected, both found by looking at screenshots:

- THE REASON MUST BE TRUE FOR WHAT IS IN FRONT OF THE USER. Taking the first
  refusal in a family printed "Transform needs a body — add or import one
  first" on a document that HAS a body, because the real obstacle was that
  nothing was selected. Now the reason comes from a verb that accepts the
  current selection and fails only on document state; if no verb in the family
  accepts this selection at all, it says "select something first" or says
  nothing. A menu whose whole value is telling the truth cannot ship a lie.
- Classification follows the level the pick cycle has REACHED, not the face the
  ray happened to hit, so the header cannot name a face while the whole body is
  lit. Sketching on the face you merely clicked is untouched — that path is
  sketch_plane_from_selection (snaporca-3a2).

Verified on :11 end to end: nothing selected shows Sketch live with Shift+S and
seven greyed rows each explaining itself; a selected solid shows Move directly
with Shift+Y (one applicable verb, so no submenu and no extra click) and five
families as submenus. Both forks compile and link.

Fork parity re-checked after the port: DesignPanel.cpp 30 divergent lines,
DesignCanvas.cpp 16, every other CAD file byte-identical — the invariant exactly.

snaporca-96r.
2026-07-31 12:20:07 +02:00
Tommaso Bianchi 00948cf767 docs: the offer is a vertical list, and it opens on every machine
Folds the form-factor decision into the charter. 4.1 is rewritten around
Tommaso's proposal — left-click selects, right-click opens a vertical list of
icon / name / shortcut — and the radial is demoted to a "Rejected" subsection
rather than deleted, because it is a good idea that loses on evidence and
somebody will propose it again. The evidence is recorded with it: mean fill of
3.45 of 8, only two live slots on a fresh document, sketch Create needing nine
addresses on an eight-slot ring, names that do not fit around a circle in
translation, and a 380px disc over the model on a 1366x768 screen.

The invariant survives the change of geometry, which is the useful proof: same
eight families, same fixed order, nothing re-sorted or compacted. It is now
stated as ROW constancy, and the one substantive gain is that unavailable verbs
are disabled IN PLACE carrying their own reason, in strings the product already
ships. An empty ring slot was mute; a greyed row teaches. On a first-run
document the offer stops being a mostly-blank control and becomes a map of what
the product does and what you must do first — which is the section 2 audience
in one picture.

Opening it is now a table rather than an assumption, because "right-click" is
not a universal gesture: two-button mouse right-clicks, trackpads two-finger
tap, a one-button Mac LONG-PRESSES or Ctrl-clicks, touch and pen long-press,
and the keyboard uses the Menu key or Shift+F10. The long-press is explicitly
an ADDITIONAL route — 6.2 forbids press-and-hold as a sole path and that
stands, so the rule now names its own exception and closes it — and it must
show that it is charging, or a user who lets go early concludes the product is
broken (L5).

Consequently: 6.2's keyboard bullet describes opening and walking the offer by
key rather than by compass direction; the gate gains question 13 (every new
pointer gesture declares its keyboard equivalent and what a one-button Mac, a
trackpad and a touch screen do) and question 12 now asks whether tool_atlas.json
was updated and the atlas regenerated; section 10 points at the rendered atlas
and says the outstanding thing is ratifying row order, not drawing the map.

snaporca-96r.
2026-07-31 11:57:53 +02:00
Tommaso Bianchi c4990ee956 docs/ux: draw the offer as a vertical list too, and it wins
Tommaso was not sure about the ring and proposed a vertical list: left-click
selects, right-click exposes icon / name / shortcut. Drawn, it is better, and
the reasons are visible in the renders rather than arguable.

THE DISABLED ROW CAN SPEAK. This is the one that decides it. A ring slot that
does not apply is an empty circle: it says nothing, and on a fresh document six
of the eight are empty. A list row that does not apply is greyed IN PLACE with
its own name and its own reason — "Create a sketch, or pick a solid face,
first", "Create a solid body to pattern first" — which are strings the product
already ships and which tool_atlas.json already carries. The first-run picture
stops being a mostly-empty ring and becomes a map of what the product does and
what you must do first. For the audience section 2 puts first, that is the
whole ballgame.

THE OVERFLOW DISAPPEARS. Sketch Create needs nine addresses; a ring of eight
pushed Polygon and Point behind a "More" slot. Nine rows is just nine rows. The
one measured defect in the ring design is not a defect in this one.

SHORTCUTS READ AS A COLUMN. Right-aligned in a list they stack into something
the eye learns passively, which is exactly the graduation path 4.1 claims —
and it is the mechanism by which the power user Tommaso describes stops opening
the menu at all. Around a ring the same keys are eight loose chips.

Also, unglamorously: long translated names fit, arrow keys and screen readers
work natively where a radial needs special handling, and a 324px box costs the
1366x768 machine far less than a 380px disc over the model.

What the ring keeps: equidistant targets and a future flick gesture. Since the
brief is that power users live on the keyboard, that buys less than it looks.

The invariant is untouched — same eight families, same fixed order, nothing
re-sorted, nothing compacted. Only the geometry changed, which is the point:
the map survived a change of form factor, so it was a real map.

Both forms are now rendered side by side for the same states, and the atlas
opens with the pairs.

snaporca-96r.
2026-07-31 11:50:40 +02:00
Tommaso Bianchi eb2fc986a4 docs/ux: the offer atlas — every tool, every state, drawn
The charter fixed the slot-constancy invariant but carried one hand-written
eight-cell table as an illustration, and nothing of the offer exists in the
product. Before any GUI code, the group needs the map itself: what verbs there
are, what each one needs before it can be offered, and what the ring actually
looks like in every situation a user can put it in.

tool_atlas.json is the source of truth and it was extracted from the code, not
from memory: verbs, shortcuts and the exact refusal strings from DesignPanel's
six feat_dropdown call sites and the sk_key table, kernel coverage checked
against CadFeatureType, headless coverage against McpControl's dispatch, and
the selection kinds taken from the callbacks DesignCanvas actually exposes. It
carries each verb's preconditions, so "why is that slot empty" has an answer
already written in the product's own words. The eventual C++ table generates
from this file too — the map exists once.

gen_offer_mockups.py renders it: 20 selection kinds x 2 document states = 40
primary rings, plus 73 sub-rings, plus comparison sheets. 113 states, none of
them hand-drawn, because a human drawing 113 rings is exactly how an address
quietly changes. Everything is framed at 1366x768, the charter's own reach
target, so L11 is tested in the mockups before it is tested in code.

Three things the drawing found that the prose had not:

- MEAN FILL IS 3.45 OF 8. The empty-slot rule is cheap in argument and
  expensive on screen; on a fresh document exactly two slots are live. That
  picture is the anti-clutter thesis made literal and it is the strongest image
  in the set.
- SUB-RINGS MUST ANCHOR ON THEIR PARENT. Fanning them from north put Extrude at
  N, which is Create's address in the primary map, so the second level
  contradicted the first. Anchored, an address is two consistent strokes: Add
  material is NE and its first verb is NE again.
- ONE FAMILY OVERFLOWS, AND ONLY ONE. Sketch-mode Create needs nine addresses
  on an eight-slot ring. That is the ninth-position pressure the charter
  predicted, arriving on schedule and measured rather than argued: either Point
  moves family, or the tail goes to a third level, or the ring is not eight.
  Every model-mode family fits. The generator refuses to wrap a tenth verb onto
  a first — silent collision is the one outcome worse than an ugly ring — and
  reports the overflow instead.

Also fixed while looking at renders: the selection pill sat on top of the north
slot's shortcut chip and hid it, and the scrim at 0.55 swallowed the very face
the ring had been opened on, which is 4.1 failing inside its own mockup.

Fork-neutral: the generator and everything it emits name no product, so both
forks carry byte-identical copies.

snaporca-2is.
2026-07-31 11:40:45 +02:00
Tommaso Bianchi c3d286070e docs: the offer is a fixed address space, not a context menu
Tommaso's requirement, and it changes what the offer IS: a tool must sit in the
same physical position whatever you selected. Click a face, an edge or a text
and fillet is in fillet's place every time. Position becomes an address the hand
learns, and the eye stops being needed.

That kills the ordering rule this section had two commits ago. "Most-used first
for that kind of selection" is adaptive ordering, and adaptive ordering destroys
the one property that makes a spatial menu fast — worse, it destroys it exactly
for the user who has just started to learn the layout. Office 2000 shipped that
idea and withdrew it. So: NO adaptive ordering, ever, in any form.

The invariant, written to survive every future feature: every tool has exactly
one address; that address is identical in every selection type where the tool
appears; slots for inapplicable tools are left EMPTY rather than compacted; and
adding a tool never re-addresses an existing one. Empty slots are the price of
constancy and they are cheap — a compacted offer is denser and unlearnable, a
sparse one is memorised in a week. An empty slot also answers a question ("this
cannot be done to this thing") that a silently-inert tool does not.

Radial rather than a strip, reversing what I proposed last time and for a reason
that only appears once constancy is the requirement: a direction from the click
point is an absolute address that survives the offer opening anywhere on screen
and survives being clamped at a screen edge, while "third item down" does not.
Centre is a hole so the picked geometry stays visible, and it names what is
selected, so a mis-pick is caught before a verb is chosen.

Every slot carries its keyboard shortcut beside the icon and the word. This is
the graduation path and it is why power users never see a conflict: you reach
for the place, the place says "F", and one day your hand types F before the ring
finishes drawing. The offer is the mechanism by which a beginner stops needing
the offer — one interface at two speeds, no advanced mode in between.

Also here: a proposed eight-position compass map across face/edge/body/text
(create, add, remove, dress-up, repeat, transform, reference, modify) offered as
the group's first ratification, with families opening a secondary ring under the
same rule; arrow/numpad direction addressing so the spatial map works from the
keyboard; a gate question 12 that treats re-addressing an existing tool as a
breaking change to every user's muscle memory.

snaporca-2is.
2026-07-31 11:10:46 +02:00
Tommaso Bianchi d7583f0a2d docs: the grammar becomes object-driven, and commit stops being invisible
Two changes to section 4, both from Tommaso.

FIRST: the selection does not merely feed the tool, it DETERMINES WHICH TOOLS
EXIST. Point at a planar face and the product offers the small set of things a
planar face can become; point at an edge and it offers fillet, chamfer and the
sketch tools that can reference it. Nothing else, because nothing else is
possible. This is the largest single thing available to us for a first-time
user, and the reason is worth writing down: a beginner's difficulty is not
operating a tool, it is not knowing which tools apply to what they are looking
at. Sixty icons answer a question they cannot yet ask; a face that offers its
own five verbs teaches the product by being used. It also deletes a whole class
of failure — a tool that silently does nothing because the selection was wrong
becomes unreachable.

The offer is an accelerator, not a toll gate: toolbar and single-letter
shortcuts keep working unchanged and consume the same selection, so an expert
never looks at the offer and a beginner never needs the toolbar. Both routes
land in the same place, which is how one interface serves all three audiences.

SECOND: "click empty space to commit" is withdrawn. It was an invisible gesture
with a destructive meaning — nothing on screen said it, and a stray click
committed a feature still being adjusted. Exactly what L5 forbids. A pending
feature now carries a confirm/cancel puck attached to its own geometry, beside
its handles, with Enter/Escape mirroring it; empty space reverts to the safe
meaning, clear the selection. The puck is an object in the scene, not a dialog:
the camera orbits, the values stay editable, nothing is blocked (L4 intact).

Two cases the rule has to get right or it damages the inner loop: continuous
tools (line, rectangle, circle) still commit each entity on its own gesture — a
tick per line would be miserable — and Enter/Escape end the tool rather than
confirm an entity. And ambiguity resolves toward keeping work: starting another
operation with a valid feature pending commits it rather than discarding it,
because undo reaches everything and the recoverable direction is the right
default.

Section 10 gains the two honest consequences: today a selection offers nothing
(the largest single item of new work this charter asks for) and committing is
still the invisible empty-space click.

snaporca-2is.
2026-07-30 21:20:27 +02:00
Tommaso Bianchi 610acfcd37 docs: reach is the first accessibility, and it gets a law
The charter had accessibility only in the assistive sense — keyboard, contrast,
colour, targets — and said nothing about who can get through the door in the
first place. That was the larger omission. The premise of an OSS CAD tool is
that a kid on a school laptop, with no licence, no account, no fast machine and
nobody to teach them, can open it and make a real thing; a tool that only the
equipped can run reaches people who were already going to design something.

So the fourteen-year-old is now the FIRST of three audiences, ahead of the maker
and the mechanical designer, with an explicit rule that when audiences conflict
the earlier one wins unless someone writes down why not.

L11 states the floor: runs completely on a low-end laptop with integrated
graphics at 1366x768, offline, no account, and no capability withheld behind a
tier, a plugin or a cloud service. Section 6 splits into 6.1 reach and 6.2 the
assistive floor: the reference machine, the small screen as the layout target
rather than the stretch case, files that belong to the user, learnable with no
documentation, plain language at the entry tier, and exploration that is never
punished — undo reaches everything, nothing asks the user to be sure.

Consequences elsewhere: the screen budget is set by the smallest screen we
serve, not the reviewer's monitor; the PR gate gains a reach question; B6 joins
the benchmark (the inner loop on the reference machine, offline, fresh install)
and every task is measured there rather than on a workstation. The side-panel
debt now fails L11 as well as L1 — on that screen the cards leave the model a
strip.

One role addition: the absent audience needs a seat. The kid cannot file an
issue, so someone owns B5/B6 and the group watches real first-timers quarterly.
Approachability is the one thing here that cannot be argued from principle.

snaporca-2is.
2026-07-30 21:17:22 +02:00
Tommaso Bianchi 76690f66e9 docs: the UX charter names only this fork's product
The doctrine is shared but the document is not: each fork's copy now speaks
about its own product only, so it reads as that project's own charter rather
than as a note about a sibling repository. This is a deliberate divergence —
the two copies must NOT be reconciled by a parity sweep. The CAD sources stay
byte-identical; only this doc branches.

snaporca-2is.
2026-07-30 21:11:50 +02:00
Tommaso Bianchi 8cc08845a8 docs: UX guidelines and charter for the Orca-CAD design group
The call with SoftFever settled that Orca-CAD is one of the branches to be
implemented and that a design+dev group forms around it. A group without a
written doctrine reviews by taste, and a CAD reviewed by taste becomes
FreeCAD one locally-reasonable side panel at a time.

So the doctrine is written first, as something a reviewer can FAIL a pull
request against: ten laws each with its own test, the interaction grammar
they compose into, the accessibility floor as a merge requirement, and a
ten-question gate answered in every UI pull request.

The position is Shapr3D's interaction economy, not its feature list —
direct, gestural, almost no chrome, depth revealed by what you touch. Depth
for mechanical designers arrives as progressive disclosure of tools that
never move, in three tiers, non-modal, with assemblies and exploded views
obeying the same point-then-act grammar as a beginner's extrude.

The one thing neither Shapr3D nor FreeCAD has is that we live inside a
slicer: plate, nozzle, material and build volume are known at design time,
so print-domain failures are warnings on the geometry, not a report.

Section 10 is an honest inventory: what already complies, and the six
things that violate the laws today, none of them defended. The appendix
keeps the anti-patterns we have already paid for, because each one is
cheap to reintroduce.

snaporca-2is.
2026-07-30 21:05:33 +02:00
Tommaso Bianchi f14d31d956 scripts/gui-session.sh: bring the headless GUI up without clicking blind
The relaunch sequence was an ad-hoc pile of docker exec one-liners, and it
had a real bug: it dismissed the first-run dialogs by computing the
titlebar close box from `xdotool getwindowgeometry --shell` and clicking
it. When the dialog had already closed, that eval left the geometry
variables stale or empty, the click landed at a garbage coordinate, and it
kept hitting the Sketch button in the toolbar underneath — so the app came
up in sketch mode with a stray Sketch feature that then had to be
cancelled by hand. Three times in one session.

The fix is not a different mechanism. `xdotool windowclose` looks cleaner
and KILLS THE APP: it destroys the GdkWindow out from under the dialog and
the process dies with "GdkWindow unexpectedly destroyed", three
GLib-GObject criticals and a segfault. Measured, not guessed — that is
what the first version of this script did. Escape does not close the Setup
Wizard either, which is why it needs handling at all. So the titlebar
click stays, and what changes is that it refuses to click geometry it has
not validated: the window id is re-resolved immediately before, all four
geometry variables are unset first and must come back numeric, and the
computed point must be inside the screen. Any of those failing logs why
and clicks nothing.

Two further honesty fixes in the status output, both caught by reading it
rather than by it failing: app_pid skipped nothing, so with a container
full of <defunct> instances it printed a dead pid as though the session
were healthy — it now walks /proc/<pid>/stat and ignores zombies. And a
container without x11vnc reported "vnc: DOWN" as if something had broken,
when nothing was ever installed; it now says so, and does not try to start
what is not there.

Also replaces the fixed post-launch sleep with a wait for the main window,
because cold starts under software GL vary by a lot, and adds --status for
diagnosis: "no windows but the desktop is up" means the app died, "cannot
connect at all" means the desktop did. That distinction cost real time to
work out by hand.

Verified on both containers: one run each, wizard closed on validated
geometry, no stray sketch mode, live pid reported, and the app still up.

snaporca-e1p adjacent (tooling, not the tab itself).
2026-07-30 14:35:35 +02:00