mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
24f4076bb581757565bc8d768df46d0dd190e90b
30128
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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). |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
3f8f46f93f |
Delete the sketch plane dropdown; the viewport decides
The plane combo is gone. A sketch takes its plane from what is picked in the viewport: a face on a solid, or one of the reference-plane ghosts, clicked in 3D. The card is now a single line of instruction instead of a control. The combo had become worse than redundant. Once a picked face could be the plane it displayed a row that CONTRADICTED the actual target — it still said XY while the sketch went onto the face — so the one place a user could look to confirm where they were drawing was the one place guaranteed to be wrong. What replaces it is state, not UI: m_ref_plane records which reference plane was last clicked in 3D (0/1/2 = XY/XZ/YZ, >=3 indexes the datums) and ref_plane_name() turns it into text for the on-geometry hint. Clicking a ghost plane while a session is live re-planes it immediately, which the combo's own handler used to do; that behaviour is kept, just driven from the geometry instead of the widget. A plane click also drops a stale face pick, so last pick wins in both directions. populate_plane_choices() stays — seven other pickers use it (Plane base, Axis A/B, Helix, Project, Mirror, Cut). Those are the next candidates, tracked on snaporca-e1p; this commit only removes the one that had become actively misleading. Also: tessellation now matches Orca's OWN STEP importer, linear deflection 0.003 instead of 0.01 (Format/STEP.hpp default; angular was already 0.5 rad and unchanged). The Design viewport was never using a different rendering technique — it hosts a real GLCanvas3D, builds a real Model/ModelVolume and goes through the same reload/GLVolume path and the same shaders as Prepare and Preview. What differed was the mesh handed to it: 3.3x coarser than anything else in the application, which is why a curved face read as faceted beside an imported part. Suite unaffected at 154 cases / 2125 assertions, so nothing depended on the old density. Verified on :10: the card shows no dropdown, one click on a face then a sketch tool still reports "on the picked face", and the circle is drawn in that face's plane (artifacts/shots/h3a2-02-sketch.png, h3a2-03-drawn.png). snaporca-e1p, snaporca-3a2. |
||
|
|
3c0843c68c |
Sketch on the face you clicked, not the one you clicked twice
The previous commit made a picked face the sketch plane and I verified it by clicking the face TWICE. That was the wrong test. handle_solid_click cycles whole -> face -> edge, and "First click on a (new) body/face selects the WHOLE solid; refine on repeat clicks" — so at level 1 m_sel_solid_face is -1, and one click on a face, which is what selecting a face means to anyone, still fell through to the plane combo. The fix was real and unreachable, which from the outside is indistinguishable from no fix at all. The face id was never missing. handle_solid_click resolves it by ray on the FIRST click and passes it to on_solid_selection_changed regardless of the cycle level; the panel simply discarded it whenever level < 2. Keep it in m_pick_face/m_pick_face_body and let a sketch use it, preferring an explicit face-level selection when there is one. Nothing about the cycle changes, so body operations that rely on whole-body selection are untouched. The new state is dropped wherever the existing picks are, so a stale face cannot come back: choosing a body from the Bodies list (an explicit choice with nothing pointed at), picking a committed sketch loop (last pick wins), undo/redo (recompute invalidates topology ids), and when a sketch consumes the face. Verified on :10 with ONE click, which is the flow that was broken: build a box, single-click its top face, S then C, and the hint reads "Circle — click center, then radius · on the picked face" with the circle drawn in that face's plane (artifacts/shots/g3a2-01-one-click.png, g3a2-02-sketch.png, g3a2-03-drawn.png). GUI-only, so the kernel suite is unaffected — plane_of_face and its 154 cases / 2125 assertions are unchanged from the previous commit. snaporca-3a2. |
||
|
|
6ce20d78c3 |
Sketch where the user pointed: a picked face is the sketch plane
Selecting a face and sketching on it is the most common gesture in solid
modelling, and it was impossible. The plane came from a combo holding
XY/XZ/YZ plus datums, and plane_from_choice had no face branch at all, so
the only route onto a face was to build a Coincident datum plane on it
first, confirm that, reopen the sketch and find the datum in the
dropdown. Three extra steps and a junk feature in the tree.
The fix is not another combo row. A new sketch now takes its plane from
what is SELECTED IN THE VIEWPORT: a picked planar face wins outright, and
only when nothing is picked does it fall back to the reference plane —
which is itself normally set by clicking one of the ghost planes in 3D,
not by opening the combo. The tool hint names the target ("Circle — click
center, then radius · on the picked face") so the choice is visible on the
geometry side rather than needing a control to read back.
CadDocument::plane_of_face is the shared derivation, so the sketch path
and the Coincident datum method cannot drift apart. It refuses
non-planar faces: face_normal_world evaluates at the mid parameter, which
on a cylinder or a fillet is a tangent plane at one arbitrary point —
fine for offsetting a datum, wrong as a sketch plane, and silently
sketching on a tangent is worse than declining.
Picking the face also CONSUMES it. Leaving the pick live meant the next
Extrude saw a selected face and push/pulled it instead of extruding the
sketch just drawn — the same trap the imported-art path already guards
against.
Verified on :10 end to end with no combo interaction: build a box, click
its top face twice to cycle whole -> face, press S then C, and the circle
is drawn in the plane of that face with its Radius tab on the geometry
(artifacts/shots/f3a2-03-face.png, f3a2-04-sketch-on-face.png,
f3a2-05-circle-drawn.png). Kernel side: 154 cases / 2125 assertions green
on both forks, including that a cylinder resolves exactly its two flat
caps and refuses the barrel.
Still side-panel-shaped and to be dealt with separately: the Plane combo
remains on the card and now merely displays a stale row when a face is
the real target. It should show the actual target or go away.
snaporca-3a2.
|
||
|
|
7f0a8c85ee |
An entity sketch that forms no wire fails, instead of extruding a default box
entities_to_wire handles exactly two shapes: one lone closed entity (Circle/Ellipse), or a chain of open ones (Line/Arc/EllipseArc/BSpline). Anything else -- a circle coexisting with a line, two circles -- returns a null wire. build_sketch_wire answered that by falling through to its legacy tail, which ends in a rectangle built from width/height. For an entity sketch those fields are whatever they were initialised to, so the extrude produced a box the user never drew, silently and with ok:true. That is how the ellipse+stray-arc case in the P2 Tier-B.1 verification turned into a default-rectangle solid. Throw there instead. The legacy profile/shape paths below are still reached by sketches that legitimately carry no entities at all, so the enum and profile constructors are untouched -- only the case where entities exist and cannot be turned into a wire now fails, which is exactly the case that was fabricating geometry. This does NOT implement the multi-loop support the issue asks for. Doing that properly means deciding containment -- a circle inside a rectangle is a hole, a circle beside it is a second region -- and make_extrude_regions cannot be reused because it takes flattened Vec2d contours for imported Text/SVG art and would discard the analytic circle. Guessing containment would trade a visible failure for a wrong solid, which is the opposite of the point. Left scoped on snaporca-88v. Also converts the three float comparisons in the two test cases added this session from Approx to WithinAbs/WithinRel, per tests/CLAUDE.md, which rules Approx out for being asymmetric and double-only. The rest of the file's pre-existing Approx uses are left alone. 153 cases / 2090 assertions green on both forks; no existing test depended on the default-rectangle fallback. snaporca-88v (partial: the silent-fallback half). |
||
|
|
5f1811c2c5 |
A subtraction that removes nothing is an error, not a silent success
A boolean cut whose tool misses the target is a perfectly legal
operation: OCCT reports IsDone(), the shape comes back unchanged, and the
feature lands in the recipe reporting ok:true. Driving the MCP socket,
that produced two consecutive {ok: true, bodies: 1, error: ''} responses
for a hole that was never drilled -- same viewport, same 46939.11 mm3 --
and the tree grew two Hole features that will never cut anything. A
caller, an agent especially, has no signal at all that the thing it asked
for did not happen.
Measure the volume across the op in route_feature's in-place branch and
refuse the no-op. Only for removals: Hole, Thread, and Extrude / Revolve
/ Sweep / Loft in Cut mode. Everything else may legitimately leave the
volume alone -- a Transform certainly does. The tolerance is relative,
because an absolute epsilon is wrong across the mm-to-metre range of real
parts, and a cut that shaves a numerically invisible sliver is a miss
too. The existing rollback in the MCP actions already preserves the
reason, so a missed hole now answers ok:false with the error and undoes
the feature.
The confusion underneath was not itself a bug: hole's x/y are in the
sketch plane's frame, whose origin is describe_scene's modeling_origin,
and describe_tools documented them only as "number, unit mm". Passing the
world centre put the hole 135 mm clear of the solid. All six x/y params
on hole / hole_styled / hole_standard now say which frame they are in,
since the wrong guess was silent.
Regression test drives the reported failure directly: a hole at x=135 on
a 20x20x20 box is rejected and leaves the body untouched, the same hole
at the origin still removes exactly pi*4^2*20, and a cut-mode extrude
whose profile sits at x=200 is rejected too. 152 cases / 2083 assertions
green on both forks.
snaporca-daf.
|
||
|
|
5d05ca30ca |
Sketch shortcuts: pick the key map by mode, not by whether a session exists
All 17 single-letter sketch shortcuts were dead. The dispatch gate was circular: the sketch key map was consulted only when m_viewport->is_sketching() was already true, but is_sketching() is a whole-session flag whose only riser is begin_sketch(), called from select_tool() -- which is precisely what every sketch key closure calls. So the first letter after entering sketch mode fell through to the feature map, where the keys are Shift+letter, matched nothing, and did nothing. The mouse worked only because the toolbar flyout row reaches select_tool() directly, bypassing the gate. Which key MAP applies is a question about the mode. Split the flag: 'sketching' (live session) still drives undo/redo, Delete and the section-view branch, where a session genuinely has to exist; a new 'sketch_mode' (m_ui_mode == UiMode::Sketch alone) drives the map choice. Verified headless end to end, keyboard only: Shift+S, then R draws a 143.4 x 133.7 rectangle on XY reporting 4 degrees of freedom, then F and L arm Fillet and Line (artifacts/shots/0udb-01..03). Note the toolbar strip does NOT change when a tool is armed by keyboard -- the family buttons are flyouts and only show their own pressed state -- so the pixel diff on that strip, which is how this was originally measured, reads 0 for a tool that is live. The status line is the surface that actually reflects the armed tool. Also fixed, from snaporca-d9i's list: the plane-pick status line said "press Sketch to draw on it", naming a button that exists only in Feature mode. It is now mode-aware. Adds a SNAPORCA_KEYTRACE=1 trace in the CHAR_HOOK printing key, ui mode, is_sketching, in_text and the focused window's class. It is what separated "the fix does not work" from "the surface being measured never moves", and it costs a full GUI build to re-add, so it stays. snaporca-0ud, partial snaporca-d9i. |
||
|
|
347b83d887 |
Sketch fillet: never write a failed solve's geometry back, and commit the op
Two P0s in the same gesture. Filleting a corner of a parametric rectangle produced either nothing at all or a sharp corner with a stray arc floating above it. Trigger (snaporca-cq2): the only routes that ever reached confirm_op were finishing the whole sketch and an unsignposted click on empty space. set_tool() dropped a ready op, so typing a radius or dragging the arrow and then touching any other tool threw the value away. Commit a ready op on tool change (before m_mode is reassigned — op_ready() and confirm_op() both switch on it), commit on Enter in the radius editor, and drop the pending op before Esc's tool downgrade so Esc still cancels rather than applies. Substitution (snaporca-pl5): libslvs writes its last Newton iterate into the params whether or not it converged, and SketchSolver read them back unconditionally, so every REJECTED solve deformed the sketch. The fillet ladder tries a deliberately over-constrained rung first (a tangent on each leg, against the legs' own H/V); it is correctly rejected, but its wreckage then failed rungs 2 and 3, which solve cleanly on their own. The arc ended up with no constraints at all, the rigid loop won, and the corner snapped shut. Measured: from pristine geometry rung 1 gives result=INCONSISTENT with 3 bad constraints, rung 2 gives dof=6 with the arc's radius intact. Read the geometry back only on success. try_add_constraints then needs no "restore" re-solve — the entities still hold the prior solved state. Kernel suite 151 cases / 2072 assertions green on both forks; the GUI check ran on the Snapmaker fork (9d72c4377a). Ported from the Snapmaker fork. snaporca-pl5 snaporca-cq2 |