Commit Graph
13398 Commits
Author SHA1 Message Date
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 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 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.
2026-07-30 14:24:29 +02:00
Tommaso Bianchi 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.
2026-07-30 14:09:23 +02:00
Tommaso Bianchi 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.
2026-07-30 13:42:11 +02:00
Tommaso Bianchi 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.
2026-07-27 07:21:06 +02:00
Tommaso Bianchi 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.
2026-07-27 00:40:25 +02:00
Tommaso Bianchi 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
2026-07-26 23:33:50 +02:00
Tommaso Bianchi 50577d66d0 Dress-up card: say whether Confirm will round the picked edge or the group
The card decides between a single picked edge and a whole face-group from a
viewport pick it never mentioned. With no edge picked the user saw only the
group combo and concluded per-edge rounding did not exist; with an edge picked
the combo still read "All" — the opposite of what Confirm would do.

Adds a Target row that names the actual target and greys the group combo out
while an edge is picked, wired at the same three points Shell already uses:
the selection-changed handler, the re-edit load, and open_tool.

Ported from the Snapmaker fork (33771a0e95). snaporca-40d
2026-07-26 23:33:50 +02:00
Tommaso Bianchi d5dee45acf Port the four DesignPanel fixes that never crossed from the Snapmaker fork
The CAD sources are meant to be byte-identical across the two forks, with exactly
two permitted divergences: DesignPanel.cpp's flyout plumbing (30 lines — mainline's
DropDown is Item-based where the other fork takes three parallel vectors) and
DesignCanvas.cpp's Bed3D::set_shape signature (16 lines). DesignPanel.cpp had drifted
to 105.

Nothing failed to announce this. The kernel suite does not compile the GUI, and all
four defects are interaction-level, so both forks stayed green while only one of them
had the fixes. The parity diff is what found it.

  * combo_append_index and its five call sites. Orca's ComboBox keeps client data in
    its own vector, so Append()'s clientData argument never reaches wxItemContainer
    and m_clientDataItemsType stays wxClientData_None. GetClientData() opens with a
    wxCHECK_MSG, which is an early return — so every read came back NULL and every
    caller resolved it to index 0, silently, because 0 is a legal answer. Affects the
    rib sketch picker, the sheet-body picker, both mate coordinate-system pickers and
    the sweep path picker.

  * Wrap(240) over the card labels. wxStaticText never wraps itself, so a one-sentence
    hint sets its card's minimum width to the width of the whole sentence and every
    control in that card is clipped at the sidebar's right edge.

  * Shell and Draft face-label initialisation in open_tool. Both read the face from the
    live pick at Confirm time, but only the pick handler wrote their labels, so picking
    a face and then opening the card left the card describing one operation while
    Confirm performed another.

  * SurfaceOffset and ThickenSurface added to build_candidate's negative list. Both read
    a sheet body from their own combo and both had it overwritten by the picked solid's
    index. The list is a negative one, so a tool that picks its own body breaks by
    omission — noted in a comment now.

Also drops a stray <cstdio> left behind by a debug probe.

Verified by building the GUI here for the first time since these landed: 461/461,
liblibslic3r_gui.a links. DesignPanel.cpp is back to exactly 30 divergent lines and
every other CAD file is byte-identical again.

snaporca-7xx snaporca-aqu snaporca-gu9 snaporca-c03 snaporca-5pl
2026-07-26 18:05:13 +02:00
Tommaso BianchiandClaude Opus 5 faed169a01 Sketch dimensions: make Tab commit, instead of silently dropping what you typed
The inline editor special-cased Escape and Skip()ped every other key, so Tab fell
through to wx's default navigation. Its popup frame holds exactly one control, so
focus came straight back to that control with its text re-selected.

Type 60, Tab, 40, Enter — expecting to fill two dimensions — and the 60 is gone: Tab
neither committed it nor advanced, so the 40 just replaced the re-selected text. A
re-selected field is pixel-identical to a freshly opened one, so nothing on screen says
a number was dropped. Tab-to-next-dimension is what Onshape, SolidWorks and Fusion do,
which is exactly why it is the key a user reaches for.

Tab now calls do_commit(), the same path Enter takes; the caller's on_commit is already
what walks to the next dimension. Verified by driving the GUI: 37 Tab 24 Enter now
produces a 37.0 x 24.0 rectangle, where before it produced 24 and a mouse-derived value.

snaporca-xah

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-26 17:48:11 +02:00
Tommaso BianchiandClaude Opus 5 295c030309 DesignPanel.hpp: declare the three wx types it uses but never named
The header forward-declares a long list of wx types and omitted wxBoxSizer, wxTextCtrl and
wxListCtrl. All three are used as pointer members only (m_expr_text, m_var_list,
m_parts_hdr, m_hdr_tree_row), so a forward declaration is all they need — but there was
none. Every ordinary build compiled anyway because the wx/panel.h + wx/scrolwin.h chain
happens to pull the real headers in transitively.

The Snapmaker fork Flatpak build (aarch64) has a wx that does not, and it failed outright:

    DesignPanel.hpp:511: error: 'wxTextCtrl' does not name a type; did you mean 'wxTreeCtrl'?
    DesignPanel.hpp:521: error: 'wxListCtrl' does not name a type; did you mean 'wxFileCtrl'?
    DesignPanel.hpp:663: error: 'wxBoxSizer' does not name a type; did you mean 'wxSizer'?

plus a cascade of "m_var_list / m_expr_text / m_parts_hdr was not declared in this scope".
Not an environment quirk: the header was simply not self-contained, which is exactly what
breaks a reviewer building in an unfamiliar configuration. The mainline fork Flatpaks passed
on both arches, so only that one manifest exposed it.

Audited the rest of the header afterwards: every other wx pointer type is either
forward-declared or genuinely included — only wxScrolledWindow and wxWindow are undeclared,
and both come from the real wx/scrolwin.h include.

snaporca-4dn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
2026-07-26 12:27:02 +02:00
Tommaso BianchiandClaude Opus 5 a95e8ee701 Re-edit: list the bodies as of the feature's timeline slot, not the final ones
Found by sweeping the index-space defect class deliberately rather than by
hitting it: that class produced 4 of the 8 defects found by hand yesterday, so
it was worth auditing every combo in the panel that maps a row selection onto
a document index.

Most of it came back clean — the loft sidecar vectors are consistent at all
four read sites, the sheet-body pickers go through the helper everywhere, mate
connectors carry client data. Six did not. A stored target_body indexes the
body list AS IT WAS just before that feature ran during replay, but Transform,
Mirror, Thicken, Rib, Project and DeleteFace all populated their combo from
the live m_doc.bodies. Boolean and Cut already replayed to the right slot.

The failure is concrete: model a body, Thicken it, then Cut something later in
the tree. A Cut replaces one body with two, so every index at or after it
shifts. Reopen the Thicken and the combo lists the post-cut bodies while
selecting the pre-cut index — showing, and on confirm re-targeting, a
different body than the feature actually used. A Boolean that consumes its
tool body shifts them the other way for the same result.

fill_body_choice() does the truncated replay populate_body_choices() already
did, for the single-combo tools. Six call sites, and 60 lines of duplicated
population loops go with them.

Visible change when testing: re-editing an early feature now lists FEWER
bodies, because it lists only those that existed then. That is correct — you
cannot target a body that did not exist yet — and it is what Boolean and Cut
have always done.

The new kernel test pins the invariant the GUI now leans on: a Cut turns one
body into two, and replaying to just before it yields the earlier, shorter
list. If body ordering after a split ever changes, that assumption fails
loudly here instead of silently in a dialog.

NOT click-tested — GUI wiring, compile-verified only. Filed as snaporca-oz7
and added to snaporca-cfi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:47 +02:00
Tommaso BianchiandClaude Opus 5 8f06b9dfd8 Design tab: make Cut re-editable, and stop misdescribing Import
on_edit_feature had two types falling into default: with "This feature type
can't be edited yet". Boolean was already handled, so the follow-up note was
stale on that point; the real gap was Cut and Import.

Cut now re-edits like any other feature: plane, offset and target body are
restored and the generic replace_feature path commits the change. The body
list is rebuilt with populate_body_choices(m_edit_index) for the same reason
Boolean does it — a Cut splits one body into two, so the live body list no
longer matches the one this feature's target index was recorded against.
Replaying to just before the feature makes the stored index land on the right
entry.

Import deliberately gets no dialog. An imported solid has no parameters to
re-edit: its geometry is rigid data read from a file, not something rebuilt
from numbers, and moving it is what the Transform feature already does.
Building an "edit" for it would duplicate Transform behind a second name. So
it now says that instead — the previous message implied a dialog was coming
that should not.

Imported 2D Text/SVG art is a different thing and stays re-editable; it
arrives as a Sketch feature carrying imported_regions and is handled above.

The default: arm is kept as a guard so a feature type added later announces
itself rather than silently swallowing the click.

NOT click-tested — GUI wiring, compile-verified only. Added to snaporca-cfi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:47 +02:00
Tommaso BianchiandClaude Opus 5 e2b58a17f7 Design i18n: extract the tab's strings at all, and translate them into Italian
DesignPanel.cpp was never listed in localization/i18n/list.txt, and xgettext
extracts only what that file names. All 934 of the Design tab's _L() calls
were therefore invisible to every translator in every language — not merely
untranslated, unextractable. Adding the one line is the actual fix; the rest
follows from it.

Regenerating the .pot brings the catalogue from 6007 to 6536 msgids. 549 of
the new ones are now translated into Italian: 5230 to 5779 translated
messages. Verified no existing work was destroyed — every msgid that survived
into the new .pot kept its translation, and the 36 that lost one are genuinely
gone from the sources. msgfmt --check-format passes, which matters here
because a large share of these carry %d / %s / %zu.

CAD terms follow Italian CAD convention rather than literal glosses: Fillet ->
Raccordo, Chamfer -> Smusso, Draft -> Sformo, Rib -> Nervatura, Mate ->
Accoppiamento, Shell -> Svuotamento, Pattern -> Serie, Sheet body -> Corpo
superficie. Strings identical in both languages are deliberately left
untranslated so gettext falls back to the msgid.

The long mixed-filament / Local-Z dithering tooltips are left untranslated on
purpose: slicer internals, outside a Design i18n task, and untranslated before
this commit too.

One string changed rather than translated. The Coord Sys hint read "Without an
edge the frame's rotation about its normal follows world X, not the body" —
that described the defect fixed in 1726e93760, so it was a lie as of that
commit. It now says X comes from the face's first edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:33:53 +02:00
Tommaso BianchiandClaude Opus 5 b25335e1b3 Rib: accept a Project feature as its sketch ref
Rib guarded with `sk.type != CadFeatureType::Sketch`, while every other
sketch consumer — Extrude, SurfaceExtrude, SurfaceRevolve, the loft paths —
tests `!= Sketch && != Project`. A Project feature carries a plane and Line
entities, which is all a rib reads, so the guard blocked "project a body
edge, then rib along it" for no stated reason.

The picker in the Design tab offered Sketch features only, so it is widened
to match: a kernel that accepts Project refs and a GUI that never lists them
would have left the path unreachable anyway.

Worth recording for whoever hits this next: Rib also needs a sketch carrying
EXPLICIT entities. A parametric Rectangle sketch (add_sketch with
width/height) has an empty entities vector — build_sketch_wire synthesises
its profile on demand — so rib_entity 0 is out of range there and it fails
with "rib: bad entity". That is why Rib could not be driven headlessly at
all before this change; a Project feature is now the one programmatic way to
produce a ribbable line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:48:16 +02:00
Tommaso BianchiandClaude Opus 5 1cb80f7f9f Project: implement "(all edges)"; stop discarding the failure reason
Two defects found while driving the tools that Phase B wired but nobody had
exercised yet.

apply_project had no all-edges branch: with no face picked and no explicit
edge list it threw "no edges or face selected". That is precisely the state
the Project card opens in, and its label reads "(all edges)" — so the card's
default could never be confirmed. It now projects every edge of the source
body. Edges perpendicular to the target plane collapse to a point when
projected, so segments whose endpoints coincide are dropped instead of being
emitted as zero-length lines that would poison the sketch downstream.

The second defect is why the first one was invisible. 29 of the 31 rollback
sites in McpControl ran `if (!ok) doc.undo();`, and undo() recomputes the
restored feature list — which succeeds and clears doc.error. Every failing
command therefore reported `error: ""`. Yesterday's fix covered 2 sites and I
treated the file as done; it was not. All 31 now capture the reason before
the rollback and restore it after. Failures that read as `""` now read as
"rib: bad entity" / "surface-revolve: revolve failed".

Verified on the running GUI through the control socket: the Project call that
previously returned ok:false now returns ok:true, and failures carry a reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:04:15 +02:00
Tommaso BianchiandClaude Opus 5 08e37296b9 Design tab: give every drawer entry a distinct icon
Six of the surface entries, both Thicken variants, Rib, Axis, Coord Sys,
Mate and Delete Face all reused a sibling's glyph, so a drawer opened as a
column of identical faces and the card that opened rarely matched the entry
clicked. Fixed both halves: entry icons are now unique within their drawer,
and each card header uses the icon of the entry that opens it.

Two new glyphs, design_thicken and design_rib, are the only ones added —
everywhere else an existing icon already carried the right meaning
(design_revolve, design_offset, design_line, design_point,
design_c_coincident, design_delete).

The Surface drawer BUTTON deliberately keeps design_surface; only its
entries may reuse the solid glyphs, because a menu row carries its own
text label while two adjacent toolbar buttons do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 06:44:16 +02:00
Tommaso BianchiandClaude Opus 5 77f2f4d4ad Design tab: datum and curve tools were rejected by the solid-preview check
refresh_preview() exempted only Sketch and Plane from the ghost-preview path.
Axis, CoordSys, Helix and Project produce no solid either, so they fell through
to it, found nothing, and reported "invalid: preview produced no geometry" —
which also DISABLED Confirm, so all four tools were unusable rather than merely
noisy. Guaranteed on an empty document; with a body present the ghost path
finds something and masks it, which is why it survived until the tools were
tried on a fresh project.

All six non-solid tools are now exempt, each with its own ready message. The
list is not a guess: recompute() skips Sketch, Helix, Plane, Axis and CoordSys
outright and routes Project through apply_project(), which emits sketch entities
and no solid — so the panel and the kernel now agree on exactly what is not a
solid. Mate already had its own branch, since it needs Confirm gated on having
two distinct CoordSys features.

Introduced when Axis/CoordSys (batch 1) and Helix/Project (batch 3) were wired
without extending this exemption. Confirmed fixed on hardware: Axis ->
Plane Intersection with XY and XZ now resolves on an empty document, which also
exercises 60b04feea1.

Compiles clean; kernel untouched, suite unaffected at 143 cases / 1980
assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 06:20:40 +02:00
Tommaso BianchiandClaude Opus 5 0455b0bf96 Design tab: give the Surface drawer its own icon
The Surface drawer used design_extrude, the same face as the Add-material
drawer, so the two buttons were indistinguishable in the feature bar.
design_surface.svg is a draped patch — deliberately unlike design_plane (a flat
parallelogram) and design_extrude (a box with an up-arrow) — with a faint
interior rule so it reads as a skin rather than a solid face. Same visual
language as the other 71: 24x24, no fill, #b6b6b6, stroke-width 0.85, round
caps and joins.

The five surface ENTRIES that also used design_extrude now use it too. That is
not cosmetic tidying: a flyout button's face follows the last-picked entry
(SetBitmap_(icon_names[i])), so changing only the drawer's default icon would
have been undone the moment the user picked anything. Surface Loft keeps
design_loft, which already suits it.

The six rows still share one glyph between them, so they are told apart by
label alone inside the flyout. Per-entry icons belong with snaporca-vrg
(Draft/Shell reusing design_dressup), not here.

Compiles clean; kernel untouched. Icon confirmed legible on hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 04:58:10 +02:00
Tommaso BianchiandClaude Opus 5 c565d6ba86 CAD: undo() never rolled back variables, so a bad one bricked the recipe
checkpoint() snapshotted `features` and undo() restored `features`, but
`variables` is a separate member of CadDocument. Every caller of the documented
checkpoint -> mutate -> recompute -> undo-on-failure pattern therefore failed to
roll a variable back: the bad value stayed in the document and every later
recompute failed, which is exactly the corruption the pattern exists to prevent.
Feature `expr` bindings were unaffected only because expr lives inside
CadFeature and rode along in the features snapshot — which is why the feature
side appeared to work.

This was a kernel gap, not a GUI one: McpControl::action_set_variable has the
same sequence and was equally broken.

The undo/redo stacks now hold a {features, variables} Snapshot. Nothing here is
serialized, so no recipe version change and no golden-fixture regeneration.

Two tests, both verified to FAIL against a faithful reproduction of the bug
(undo() leaving `variables` untouched) at test_caddocument.cpp:4420 and :4441:
one covers restoring a variable's previous value, the other covers removing a
variable that did not exist before the checkpoint. Worth recording that the
first mutation attempt was NOT faithful — it dropped the restore but kept
std::move(variables) into the redo stack, which empties the map as a side effect
and made the second test pass for the wrong reason. A mutation has to reproduce
the original defect, not merely break the code.

Second defect, same area: undo() calls recompute(), which succeeds and clears
doc.error, so the reason an edit was rejected was destroyed before anything
could display it. Six sites — four in DesignPanel, two in McpControl — now carry
the message across the rollback. on_remove_variable additionally asserted
"referenced by a feature expression" as fact; it now offers that as the likely
cause and appends the real error, since that diagnosis is wrong for any other
failure.

Suite 141 cases / 1972 assertions (was 139/1960). GUI compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:56:34 +02:00
Tommaso BianchiandClaude Opus 5 33275f2c74 Design tab: sheet pickers targeted the wrong body; guard Delete Face's face list
The sheet-only pickers filtered correctly and then threw the filtering away. Their
rows are the SHEET bodies, but GetSelection() was passed straight through as an
index into m_doc.bodies. With a solid at 0 and a sheet at 1 — the normal order,
since you extrude a solid before making a surface — the single row is row 0 but
body 1, so Surface Offset and Thicken Surface targeted the SOLID. The kernel then
refused with "target is not a sheet", which reads as a kernel bug rather than a
picker bug, and the row's own label ("Body 2") disagreed with what was targeted.
Four sites per tool were wrong, including the re-edit path, which compared a body
index against the sheet-only row count and so restored the wrong row.

populate_sheet_body_choices() now carries the real body index in client data, and
two helpers make the row/body distinction hard to get wrong again:
sheet_choice_body() reads it back, select_sheet_choice() finds the row holding a
given body. No caller touches GetSelection()/SetSelection() on these pickers.

This is the third instance of the same index-space confusion in this file, after
the 0-based body labels in the interference report and the Rib sketch picker. The
kernel suite cannot catch any of them: the kernel receives whatever index the GUI
computed, and its own tests pass correct ones.

Delete Face was structurally right — its picker uses the all-bodies populate, so
its indices genuinely match, and accumulation appends with a running list. Two
gaps closed: clicking "Add picked face" with nothing picked was a silent no-op,
indistinguishable from a broken button, and the same face could be added twice,
putting a duplicate id into delete_faces that the defeaturing has no reason to
cope with. Re-adding is now a no-op with a message, not an error.

Both confirmed working on hardware. Kernel untouched: 139 cases / 1960 assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:46:14 +02:00
Tommaso BianchiandClaude Opus 5 8621f1168e Design tab: show/hide the printer bed; give Placement its own toolbar slot
Port of snaporca ca25352d74. Hand-applied rather than cherry-picked: unlike the
Phase B commits, which only touched DesignPanel.{cpp,hpp} and transfer verbatim,
this one reaches into GLCanvas3D and DesignCanvas, where the forks genuinely
differ — mainline passes m_show_world_axes to _render_bed where Snapmaker passes
a local show_axes, and the surrounding code sits ~90 lines further down. git am
refused, correctly; the six edits were applied against mainline's own context and
the DesignPanel half came across as a patch.

Bed toggle: a "Bed" checkbox in the document/view row, on by default, in that row
rather than in a card because a view option must stay reachable with no tool open.
It drives GLCanvas3D::m_show_bed (default true, so Prepare and Preview are
untouched) and gates _render_platelist as well as _render_bed — hiding the bed
while leaving its grid and outline floating would read as a rendering fault.

Bound to wxEVT_TOGGLEBUTTON, not wxEVT_CHECKBOX: Orca's CheckBox derives from
wxBitmapToggleButton, so a wxEVT_CHECKBOX handler never fires.

Also gives the Placement drawer its own "placement" toolbar slot. put("place")
already holds the Place-on-Face button, and put() formats slot item 0 as the
control and later items as its chevron, so sharing the slot bottom-aligned the
drawer's button like a chevron.

196/196 targets, 0 compile errors, orca-slicer links (165 MB). The build script
still exits non-zero at the AppImage bundling step on libpython3.12.so.1.0 —
that is snaporca-96t, packaging only, and does not affect the binary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 18:33:33 +02:00
Tommaso BianchiandClaude Opus 5 d4904b8e2e Design tab: stop eight tool cards rendering at startup; regroup the drawers
The sidebar opened with eight tool cards stacked in it — Transform, Mirror,
Thicken, Rib, Project, Delete Face, Helix and Mate. A card added to the cards
sizer is visible until something hides it, and close_tool()'s hide-all only
runs on a tool SWITCH, so anything missing from the construction-time hide
block is on screen from the moment the tab opens. Those eight were wired into
close_tool() but never added here. All 37 cards are now hidden at startup.

Worth stating because it invalidates a check I ran while diagnosing this: every
card IS hidden somewhere in the file, so grepping for "hidden anywhere" says
nothing. The block that matters is the one in the constructor.

Second, the drawers mixed unrelated operations, and two group tooltips no
longer described their contents — Dress-up listed eight tools spanning three
different kinds of operation, and Add material still claimed to hold only
extrude/revolve/sweep/loft after Thicken and Rib were added to it.

One concept per drawer now:

  Add material   extrude, revolve, sweep, loft, thicken, rib
                 -> grows new solid material, whether from a profile, a face or
                    a line
  Surface        unchanged; already coherent
  Datum / Curve  plane, axis, coord sys, helix, PROJECT
                 -> reference geometry and derived curves. Project consumes a
                    body but PRODUCES sketch entities, so it is curve creation,
                    not a finishing operation
  Placement      TRANSFORM, MIRROR, MATE                              (new)
                 -> moves a body without changing its shape; a mate places one
                    body relative to another
  Dress-up       fillet/chamfer, draft, shell, delete face
                 -> finishing on the faces and edges of an existing solid
  Hole / thread  unchanged

The new drawer costs no toolbar width: the layout order already contained an
empty put("place") slot between "material" and "plane" with nothing registered
to it. Shift+Y and Shift+Z follow Transform and Mirror; every tool still
appears exactly once.

Compiles clean; kernel untouched, so the suite is unaffected at 139 cases /
1960 assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:39:53 +02:00
Tommaso BianchiandClaude Opus 5 6372b3b505 Design tab: M6 variables panel + per-feature expression bindings
M6 landed the kernel side as two plain public maps — CadDocument::variables and
CadFeature::expr — reachable only through MCP's set_variable / set_feature_expr.
Nothing in the GUI could create a variable, so the parametric layer was
unreachable from the Design tab.

Variables get a wxListCtrl (name, expression) with add/edit/remove below the
feature tree, since they are document-scope and must not live in a card that
only exists while a tool is open. Expression bindings get one generic row in the
feature-edit path — a field-name combo plus an expression box — rather than an
extra control on each of 32 cards.

Every mutation copies McpControl's sequence exactly, and the rollback is the
part that matters: checkpoint, mutate, recompute, undo() on failure. Without it
one typo leaves the recipe permanently unrecomputable, since load() replays the
whole list. Removing a variable a feature still references fails that recompute,
so it reports the reference rather than a bare evaluation error.

The field-name combo is deliberately editable: only 11 feature types get a
curated field list, and free text is what makes the other 21 reachable. That is
safe because assign_field() throws "unknown parameter: <name>" for anything it
does not know, inside recompute()'s try block — so a wrong name gives a clear
message and a rollback, never a silently dead binding.

Two fixes on top of the generated wiring:
- make_combo() passes wxCB_READONLY, under which Orca's ComboBox HIDES its text
  ctrl (ComboBox.cpp:51). There is no SetEditable() to undo that, so the field
  combo is constructed directly with style 0; that shows the ctrl with
  wxTE_PROCESS_ENTER and makes GetValue() return typed text.
- the field-list helper had been made a file-static function taking
  DesignPanel::Tool, which required moving Tool out of private and into the
  public API. It is now a private static member instead: 32 values of internal
  card state should not be published to satisfy a signature.

Compiles clean (0 errors); kernel suite unchanged at 139 cases / 1960
assertions. Phase B is complete on this fork — all 16 previously GUI-less tools
plus the variables panel. Not yet exercised on a display.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:14:32 +02:00
Tommaso BianchiandClaude Opus 5 cf97b7aba8 Design tab: Mate tool + interference report — the 16 tools are now reachable
Mate completes the set: every CadDocument feature type now has a card. It is the
one that needed CoordSys wired first, since a mate connector IS a CoordSys
feature and cs_a/cs_b are feature indices into the recipe.

The card states what each kind constrains rather than just naming it, because
the five kinds are not distinguishable from their labels — the useful fact is
which DOF each PRESERVES: Fastened fixes all six, Planar leaves in-plane
sliding, Revolute leaves spin, Slider leaves axial travel, Cylindrical leaves
both. The offset/angle spins retitle per kind, since offset is a plane distance
for Planar and a position along the axis for the three joint kinds.

Confirm is blocked, with the reason in the status line, when fewer than two
CoordSys features exist or when A and B are the same one: a mate with cs_a ==
cs_b is meaningless and a dangling index recomputes to nothing useful.

check_interference() gets a button in the feature-tree header behind a rule, not
a tool card — it adds no feature, so it must not checkpoint(), recompute(), or
touch the undo stack, and a card would imply it does. Results go to the status
line as count + worst volume, with the per-pair list in a message box, named as
the parts tree names them.

Fixes on top of the generated wiring:
- the button's sizer adds sat after the closing brace of the block declaring
  trow, so trow was out of scope ("'trow' was not declared in this scope");
- the CoordSys client data was typed const void*, which Append rejects;
- the report labelled bodies 0-based while all 28 other body labels in this
  panel (and the parts tree) are 1-based, so it would have called the tree's
  "Body 2" an interference on "Body 1" — and it was the only unlocalised label.

Compiles clean (0 errors); kernel suite unchanged at 139 cases / 1960
assertions. Still to come: the M6 variables panel. The GUI has not yet been
exercised on a display.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:14:32 +02:00
Tommaso BianchiandClaude Opus 5 9f01c78456 Design tab: GUI for 15 tools that only had an MCP method
Every CAD feature added across M1-M8 got a kernel API and an MCP method, and
almost none got a card. The MCP path was the only way to reach them, so from
the Design tab these features did not exist: add_axis and add_coordsys have
been callable since M1 with no UI at all.

Wired here, in three batches:

  datum    Axis (5 construction types), CoordSys
  surface  Surface Extrude / Revolve / Loft / Fill / Offset / Thicken Surface
  body     Transform, Mirror, Thicken, Rib, Project, Delete Face, Helix

Grouped into existing dropdowns rather than widening the 16-slot toolbar:
surfaces get one new "Surface" dropdown, body ops join Dress-up, Thicken/Rib
join Add-material, Helix joins Datum (renamed "Datum / Curve"). Shift+Y and
Shift+Z went to Transform and Mirror; the remaining five are shortcut-less
rather than getting invented chords.

Three places where the UI has to encode a kernel distinction, not just expose
a field:

- Surface Offset and Thicken Surface consume a SHEET body and fail with "target
  is not a sheet" on a solid, so their pickers filter on
  CadDocument::is_sheet_shape() and say so when no sheet exists. Thicken (solid
  face -> plate) is a different tool and is kept visibly separate.
- delete_faces is a vector, so Delete Face accumulates picks via "Add picked
  face" and shows the running list. Supporting one face would have been a
  silent downgrade of the kernel field.
- CoordSys labels its edge pick with the consequence of omitting it: without an
  edge, datum_frame() takes x from coordsys_x_hint (world constant) and the
  frame cannot express rotation about its own normal — which is snaporca-en4,
  and is why a Fastened mate built on a face-only connector cannot fix spin.

The Rib sketch picker needed the 3-arg Append(text, wxNullBitmap, clientdata):
ComboBox's own Append(text, bitmap) hides wxItemContainer's (text, void*), so
the 2-arg call resolves to the bitmap overload and fails with "conversion from
void* to const wxBitmap is ambiguous". The Sweep picker already documents this;
Rib now matches it.

Compiles clean (0 errors) against snaporca-deps; kernel suite unchanged at
139 cases / 1960 assertions. Mate, the interference report and the M6 variables
panel are still to come; the GUI itself has not been exercised on a display yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:14:32 +02:00
Tommaso BianchiandClaude Opus 5 8c6df84acc Merge upstream/main into cad-mainline (530 commits)
Catches the fork up from 449a4cf9fc (2026-06-28) to d6cb667b89 (2026-07-24).
Upstream touched 2326 files; 17 of them overlap the 164 this branch touches.

16 of the 17 auto-merged, including all three CMakeLists.txt, the build_all.yml
CI workflow, and every GUI file. The CAD core never conflicts: CadDocument,
SketchEngine, SketchSolver, McpControl and test_caddocument are files this fork
adds, so upstream does not touch them.

The one conflict, tests/libslic3r/test_3mf.cpp, was purely additive in all three
hunks and is resolved as a union: our test pinning that store_bbs_3mf embeds the
CAD recipe as Metadata/SnapOrca_cad.bin, upstream's multi-nozzle plate-metadata
round-trip tests, and both sets of includes. All three were verified present
after resolution rather than assumed.

NOT BUILD-VERIFIED, for a reason that predates this merge and is not caused by
it: this fork cannot be configured on nativedev at all. Its CMakeLists has
required Eigen3 5.0.1 since before the merge (line 592 pre-merge), while the
only deps image on the machine is snaporca-deps, built for snaporca's
find_package(Eigen3 3.3). CMake fails at configure, so nothing compiles.

That means this fork's Catch2 suite has never run. Every "suite green" figure
recorded for M1-M8 was snaporca's suite; the ports were verified by patch-apply
plus the CAD sources being byte-identical to snaporca's. Building an orca_cad
deps image with Eigen 5.0.1 is what would finally close that gap.

Pre-merge state is preserved at branch cad-mainline-pre-upstream-2026-07-25
(30d54f0074).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:28:33 +02:00
Tommaso BianchiandClaude Opus 5 30d54f0074 M8c: interference detection
check_interference(min_volume) reports every pair of solid bodies whose
intersection encloses more than min_volume, as {body_a, body_b, volume}. It
reports only — no geometry is mutated, so calling it cannot disturb mates or
placements. Read-only at the MCP surface too: no checkpoint, no recompute.

Sheet bodies are skipped up front: an intersection involving one encloses no
volume, so the boolean would be wasted work. Bodies that merely touch share a
face and enclose nothing, so face-to-face contact is not an interference.

A boolean that fails on one pair must not lose the report for every other pair,
so each pair is guarded — and OCCT raises Standard_Failure, which is not a
std::exception and would otherwise escape.

No new serialized fields, no recipe bump: this reads `bodies`, which is
recompute output and was never serialized.

No separate MCP listing for instances and mates: describe_scene already emits
the feature tree, and Mate has rendered there correctly since M8a fixed
feature_type_name.

Tests assert the exact overlap volume (20*20*4 = 1600 mm^3), both negative cases
(clearly apart, and exact face contact), that sheets are skipped, that the
min_volume gate silences a real overlap, and that a clash created by a Fastened
mate is detected — which ties the M8b placement work to this report.

Suite 139 cases / 1960 assertions green. McpControl.cpp is reviewed but not
compiled by kernel-test.sh, which builds only libslic3r_tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:15:30 +02:00