Carries snaporca 95e59289f9, faec177d42 and 20df726ecb. Parity re-verified: 17 files identical,
8 diverging by their expected counts — DesignCanvas.cpp back to 16 and DesignPanel.cpp back to
32, which is the proof each hunk landed on the right side of the FeatFlyout and TAB_ID_PREPARE
divergences rather than on top of them.
All three answer exussum12's review on OrcaSlicer PR #15238.
ENTER/ESC IN THE VALUE FIELD. The field is a borderless always-on-top frame, and whether it may
hold keyboard focus is the platform's decision — a borderless NSWindow can never be key, and
mutter refuses a re-mapped window. When focus is denied the keys reach the panel instead and the
queued-dimension chain (a line queues Length then Angle) cannot be walked. The CHAR_HOOK now
forwards Enter/Numpad-Enter/Tab/Esc to the field when it is open and unfocused, and stays out of
the way when it is focused.
ESC FROM ANYWHERE. Separately and more simply: `dismissable` is false throughout sketch mode
because m_active is the FEATURE tool, so Esc fell through to whatever widget had focus. Click
any toolbar button or the Construction checkbox first and Esc did nothing at all — the likelier
reading of "Esc hardly ever works", and platform-independent. DesignCanvas exposes
request_sketch_exit() and the hook calls it whenever a sketch is live, after the inline-field
forwarding so an open field still takes Esc first.
RENAME. wxTR_EDIT_LABELS plus the two label-edit events write through to CadFeature::name and the
recipe, with a Rename verb in the offer and F2. The rebuild is deferred with CallAfter because
refresh_tree() destroys the very wxTreeItemId wx is holding during END_LABEL_EDIT — inline, it
killed the process.
STALE PICKS. set_tool now drops the Dimension tool's first pick, the Constrain picks and
m_point_sel, and delete_selected clears the pending dimension reference that could otherwise
dereference a renumbered entity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 39fac9b725. Parity re-verified: 17 files identical, 8 diverging by their
expected counts, DesignPanel.cpp still at 32 — the mirrored files were copied and the two
divergent ones patched hunk by hunk, so the counts returning to their expected values is the
proof each landed on the right side.
All 90 offer verbs are now firable by name over the socket, which matters because a deck key
can only send a keystroke and 49 of them have no shortcut at all. sketch_set_value calls the
same apply_dimension the in-canvas value field calls, so a typed dimension can be asserted with
no window manager in the way.
Three guards came with it, each confirmed against the source: on_mass_properties bounds-checks
m_sel_solid_body (it defaults to -1, and run_verb bypasses the menu grey-out that used to hide
that); sketch_set_value validates its value at the boundary because apply_dimension records a
driving constraint even for values it refused to apply; and run_verb refuses btn:/fly: verbs
that do not apply to the selection while leaving key: verbs alone, so the socket offers exactly
what the GUI offers. Dispatch is deferred through CallAfter so no modal verb can wedge the
socket thread.
GUI target builds and links against the rebuilt deps image.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Carries snaporca 971320e129, 6b049f0dc6, 4aae782029, 444d59f212, 74cf3d7e54 and the build
guards from 597557a6e4. Parity re-verified after every hunk: 17 files identical, 8 diverging by
their expected counts — DesignPanel.cpp still 32, DesignCanvas.cpp still 16, which is the proof
each hunk landed on the right side rather than being copied over a real divergence.
OFFSET OFFSETS THE CHAIN. Per-entity offsetting returned a closed rectangle as four parallel
segments that no longer touch, so entities_to_wires gave four OPEN wires and nothing could be
extruded. offset_entities now chains by shared endpoints and repairs each seam by mitering the
neighbours to their intersection. Second bug, invisible to any single-entity test: +d meant
"left of travel" for a line but "radius + d" for an arc regardless of sweep, so a slot outline
offset with its straights going one way and its caps the other. The convention is now written on
the declaration and pinned by a test.
tests/libslic3r/test_sketchprofile.cpp is new and asserts the LOOP rather than coordinates —
the property that decides whether a profile can be built, and the one the existing single-entity
[SketchEdit] cases cannot see. Its include is catch2/catch_all.hpp here: this fork ships Catch2
v3 while snaporca is on v2, which is why the test files are a tolerated divergence.
RIGHT-CLICK PICKS WHAT YOU POINTED AT, so a line's own verbs are offered instead of the
empty-selection vocabulary; sk_delete stops sharing btn:delete with the feature tree; and an
element's defining number (length / radius / diameter / angle / distance) can be typed, from the
menu or from V.
TWELVE MCP SKETCH VERBS. The socket had ~40 verbs and none touched a sketch, so the 2D layer
could only be exercised by driving a GUI with synthetic clicks. sketch_describe reports each
closed loop, the loops it encloses as voids, exact areas, and where a chain is still open;
sketch_validate/sketch_heal are FreeCAD's ValidateSketch — find vertices that overlap within a
tolerance but carry no coincidence, then weld them AND record the constraint, so a loop closed
by floating-point luck becomes one closed by construction. scripts/mcp-sketch-smoke.py is the
loop that asserts all of it.
Kernel suite on this fork: all tests passed, 2677 assertions in 230 test cases. The GUI target
links against the rebuilt deps image (the wxInspector blockage is gone) and the binary carries
the new verbs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were mouse-only: Commit to Plate is a toolbar button bound to wxEVT_BUTTON,
the bed is a CheckBox, and neither had an accelerator. That put them out of
reach of anything driving the keyboard, and out of reach of a hand that had not
already left the model to find them.
Ctrl+Shift, because the Shift+letter space is full to the last letter and
because the char hook deliberately ignores every Ctrl-combo -- which is exactly
what leaves this layer free to claim. P is Plate and B is Bed; neither collides
with OrcaSlicer own Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate), and
nothing else in the tree binds either.
The lookup goes ahead of the guard that drops Ctrl-combos, and nothing already
bound changes meaning: a plain Shift+letter still resolves as before, because
the new layer only answers when Ctrl is held as well.
The bed toggle drives the checkbox rather than the viewport alone, so the
control and the view cannot disagree about what is shown, and it says which it
did in the status line.
Both verified on the running build: Ctrl+Shift+B toggles the grid and the
checkbox together, Ctrl+Shift+P commits to Prepare.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported on PR #15238: on macOS, drawing a corner rectangle and pressing Enter
on the first dimension opens the second field and then wedges the entire
application — no typing, no Escape, dead menu bar, no tab switching, and the
field stays composited over the desktop after minimising. That last detail is
what identifies it: an already-drawn window keeps being composited by the
WindowServer once the process stops answering. The main thread is stuck.
4c8c93b512 is the only commit that ever changed the second-queued-field path.
It stopped unmapping the value field between two queued dimensions, and its
whole justification is mutter: focus-stealing prevention refuses keyboard focus
to a window that was just re-mapped, which left the second field visible but
dead on GNOME (snaporca-p8uw). It was applied with no platform guard, so macOS
re-activates an already-visible borderless NSWindow at NSModalPanelWindowLevel
from inside wxOSX's pending-event drain — which on macOS runs from a
CFRunLoopObserver at kCFRunLoopBeforeTimers (evtloop_cf.cpp) over an unbounded
`while (!m_handlersWithPendingEvents.IsEmpty())` (appbase.cpp).
One constexpr now carries that choice, and both halves of the contract read it,
because the failure mode of this fix is the two halves drifting apart: open()'s
reuse-if-shown and do_commit()'s deferred hide are one decision, not two.
Not a macOS guess I could not check: the non-GTK branch was exercised on the
Linux rig by forcing the constant to false and rebuilding. A corner rectangle
drew, its first field committed at 60 mm, the SECOND field opened, committed at
40 mm, and the sketch ended at 60.0 x 40.0 mm with the field closed and no
freeze — so the map-afresh path is functionally complete, not merely different.
On GTK the constant is true and every generated instruction is unchanged.
What is still unproven is the exact line where macOS wedges; the reporter has
been asked for a `sample` of the hung process. This fixes the cause the evidence
points at without waiting for that, and cannot regress the GTK behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review request on PR #15238: "Place CAD-related files (e.g. CadDocument/
GeometryEngine) into a separate folder."
src/libslic3r/CAD/ the kernel — CadDocument, GeometryEngine, the four
Sketch* units, SketchSolver, ThreadStandards
src/slic3r/GUI/CAD/ the tab — DesignPanel, DesignCanvas, DesignSketchTool,
SketchInlineEditor, McpControl, generated DesignOffer
Pure relocation: no line of logic changes. Two include rewrites follow from it —
files that moved re-spell their own neighbours against src/ (already on the
include path), and files that did not move pick up the new folder. docs and
docs/ux/mockups/gen_offer_table.py follow the same paths.
Verified: libslic3r, libslic3r_gui and libslic3r_tests all build, CAD suite green
at 2518 assertions in 194 test cases, and the sibling fork builds identically —
17 shared sources still byte-identical, 8 diverging by their expected counts.
Selecting the web Device tab loaded the printer's web UI from the selected discovered machine
when the preset carried no host. That arm was lost merging main into this branch — two of the
three Plater.cpp hunks from #15134 survived, this one did not — leaving the tab blank, since
PrinterWebView starts on an empty URL and nothing else navigates it.
In printer-agents mode the legacy web page was appended under Notebook::PAGE_MONITOR, which
resolves to the same "monitor" id as the native Device tab. FindPageByName returns the first
match, so PluginPages::relayout() — which saves the selection by name and restores it after
rebuilding the tab strip — moved the user off the web tab onto the native one. The tab also
disagreed with its own label, being created as "Device (legacy)" and renamed to "Device (Web)"
on the next show_device() call.
The last unbuilt element of snaporca-wgsc. Two connectors a mate binds are one
object with a gap still in it; drawn as two separate frames they read as
unrelated, and "which two of these five frames are the mate?" had no answer on
screen at all.
Dashed, grey, drawn IN WORLD along the segment joining the origins -- so it
foreshortens with the model and its length is the gap the mate has left to
close. A Fastened mate therefore draws nothing, which is correct: the gap is
zero. Screen-constant dash pitch (6 px dash, 4 px gap) like every other gizmo
here, with the pitch opening up beyond 400 dashes so a mate across a large
assembly cannot emit thousands of segments. Depth test off: the line's job is
to say "these two belong together", and it has to say it even when a part sits
between the camera and one end.
Fed from BOTH sources of polarity truth, the same two the role colours already
use: every committed Mate feature (connectors named by feature index), and the
live pick of an open Mate card (named by combo ordinal). Only resolved frames
are eligible, so an unresolved end draws no line rather than a line to the
origin of the world.
RIG-VERIFIED on :10 with the fresh binary (/OrcaSlicer/build/src/Release,
2026-08-17 12:43): two imported bodies, a face-and-direction connector on each,
Planar mate offset 40. Sampling the segment between the two origins gives a
regular dash/gap alternation of 13:10 sample units -- the 6:4 px pitch -- in the
stroke grey (107,117,133), over both solids. The grey end keeps its open collar
head and the blue end its filled one, so polarity and pair now read together.
Refs snaporca-wgsc
Tommaso's decision (snaporca-x0kd): face orientation is hardwired perception -- a toddler reads
a face's roll and verse with no instruction -- so the connector is a face by default and the
conventional disc + roll quadrant stays, selectable, for users who expect it.
Preferences > Control > Camera > "Draw mate connectors as a face", default ON, key
design_connector_face_glyph. Read every frame rather than latched, so toggling takes effect on
the next repaint -- a look you cannot A/B without restarting will not get compared. Verified on
the rig: unchecking it switches the viewport to the disc live, no restart.
WHY A RELIEF AND NOT A DRAWING. A flat face in the connector's plane foreshortens by
sin(elevation) and collapses at a grazing view exactly like the quadrant it replaces -- measured,
the quadrant falls 89 -> 20 -> 3 -> 0 lit pixels from 47 degrees to edge-on. The relief does not:
its silhouette carries the information. So the glyph is a small shaded solid, painter-sorted,
lambert-shaded against a light fixed in CAMERA space so orbiting does not swing the shading.
THE MUZZLE, AND THE MISTAKE THAT NEARLY LOST IT. It is the only feature standing along +Z, so it
says which way the connector points and it is all that survives edge-on. Two errors on the way:
1. I built its footprint from height*tan(draft) and got a needle. The real base OVERHANGS the
crest at both ends (0.062 nose, 0.034 tail) and that overhang is what makes it a wedge. Base
now lifted straight off the mesh.
2. Worse, I chased fidelity. Scaled honestly the ridge is 11.3 mm on an 83.3 mm face -- 13.6 %
of the width -- and at 22-48 px that is a scratch. Tommaso looked at it and could not find
the muzzle at all, which is the only test that counts. A glyph is a symbol, not a scale
model, so it now gets two deliberate exaggerations, and COLOUR does most of the work:
muzzle share of lit pixels at 90/16/6 deg -- body tone 14.8/11.3/17.5 %, accent gold
18.3/19.2/23.9 %, accent gold at 1.8x width 23.5/25.2/31.2 %.
The accent is the same gold the disc spends on its roll quadrant, so it stays this tab's "here
is the direction that matters" colour. Polarity is still on the Z arrow's head; nothing collides.
A connector whose ROLL COULD NOT BE DERIVED keeps the disc treatment whatever the preference says.
A face asserts a definite orientation, and asserting one for a roll that was never derived is the
same confident lie that got billboarding rejected.
Geometry is emitted from the part by docs/design/mate-connectors/emit_glyph_table.py, not
hand-drawn, so glyph and printed connector cannot drift: 12-vertex outline, two eyes, chin bar,
cheek dot, and the snout wedge. Crest 29.0 mm / 6.58 mm drop / 13.1 deg against the review's
28.3 / 6.61 / 13.1 on the B-rep.
Also fixes extract_outline.py, which walked w.Edges: OCC returns them in storage order, not ring
order, ignoring per-edge orientation, so the outline was scrambled -- 45 points and perimeter
6.380 where a clean ring gives 31 and 3.335. Every measurement in the design notes was re-run.
The correction reversed one earlier finding: handedness does NOT read on its own (5.4/8.0/9.1 %
different from its mirror, not the 32-35 % the scrambled ring produced), so the cheek dot is
required rather than merely nice.
RIG-VERIFIED on Xvfb :12 against a 60x40x10 box with a face+edge connector: the face renders with
both eyes, ears, chin bar, cheek dot and a gold muzzle standing proud; the Z arrow degenerates to
its ring when viewed down the axis; and the preference switches to the disc live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, found by driving the app: clicking Login /
Register on a fresh install pops "You are currently in Stealth Mode. To log
into the Cloud, you need to disable Stealth Mode first." — to a user who has
never touched the toggle, whose config says stealth_mode: false.
It is the same pre-wizard latch one step earlier. handle_web_request() gates the
login commands on get_stealth_mode(), which reports stealth while
firstguide/finish is unset, so the app blocks sign-in because the wizard is
unfinished — backwards, since signing in is how a user leaves that state.
Worse, the escape it offers does not work. "Quit Stealth Mode" writes
stealth_mode = false, which was ALREADY false, and never touches the latch: the
config is byte-identical afterwards and get_stealth_mode() still returns true.
The user clicks the button, believes stealth is off, signs in, and finds every
cloud feature still dead. That is the state the reporter of #15239 described.
So the login guard now reads the user's OWN setting via the new
get_stealth_mode_setting(), not the pre-wizard default. A user who deliberately
enabled Stealth mode still gets the dialog and the working Quit button; a user
who merely closed the wizard goes straight to the login page.
Measured on Xvfb with a fresh datadir (firstguide absent, stealth_mode false):
before, clicking Login produced a "Stealth Mode" window; after, it opens the
"Login" window directly. And with the previous commit's latch release, a real
Orca Cloud sign-in on that same unfinished-wizard profile now runs the whole
post-login flow — the sync prompt fires (sync_user_preset lands in the config),
the per-user preset folder is created, and Sync Presets syncs with no refusal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported on #15239: after signing in to Orca Cloud on a fresh install, no sync
prompt appears and File > Sync Presets is greyed out with nothing to explain why.
CAUSE. AppConfig::get_stealth_mode() returns true whenever `firstguide/finish` is
unset, and that flag is written in exactly one place — GuideFrame::SaveProfile(),
i.e. only when the setup wizard is COMPLETED. Closing the wizard is what people do
today to reach the login (the wizard never offers it, which is #15239 itself), so a
new user ends up permanently in a stealth mode they never chose. Every cloud gate
keyed on get_stealth_mode() then switches off silently, including:
* GUI_App::on_user_login_handle(), which returns EARLY on stealth — so the whole
post-login flow is skipped: preset migration, plugin fetch, user-preset load and
show_sync_dialog(). That is the missing sync prompt.
* the Sync Presets item in both the top menu and the File menu, whose enable
lambda was `is_user_login() && !get_stealth_mode()`. That is the greyed item.
The result is indistinguishable from real Stealth mode, and nothing in the UI says
so, because the one place that DOES explain it — the "Quit Stealth Mode" dialog in
handle_web_request() — only covers the homepage login commands.
FIX, two parts.
1. The pre-wizard value is a DEFAULT for "the user has not been asked yet", not a
setting, so it must not survive the user answering. Signing in to a cloud account
is that answer. AppConfig now carries a session-only `m_cloud_logged_in` mirrored
from the network agent (on login, on logout, and at agent start so a restored
session counts), and get_stealth_mode() consults it before falling back to the
pre-wizard default. An explicit Stealth mode setting is untouched and still wins:
a user who turned it on deliberately stays offline whether or not they sign in.
2. Sync Presets no longer greys itself out. Both refusal paths already had a message
to show — "You must be logged in…" and now one for Stealth mode naming the
Preferences toggle — and the enable lambda was making both unreachable. A disabled
item that cannot say why is the reason this took a bug report to find.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
snaporca-x1k7 filed three Transform defects. Two were real and are fixed here;
the third does not reproduce and is withdrawn with its measurement.
(1) The feature tree had no ITEM_ACTIVATED binding at all, so double-clicking any
row only highlighted it. Double-click is the documented edit gesture elsewhere
(a committed sketch opens that way on the canvas), which made every feature look
dead until the user found the Edit button in the section header. Bound to
on_edit_feature(), so the gesture now works for every feature type, not just
Transform.
(3) The Transform card's typed fields called refresh_preview(), but preview_fields
returns {} for Transform and the solid-preview path has no ghost to build for a
feature that moves an existing body — so typing a distance changed nothing on
screen until Confirm. The fields now drive the same channel the gizmo drag already
uses: the body's display transform. In EDIT mode the committed transform is
already baked into the kernel geometry, so the preview undoes it first; without
that term, re-opening a committed Z=20 and typing 40 would show the body at 60.
(2) NOT REPRODUCED. Dragging a rotation ring does fill the field: a tangential
drag on the red ring gave Rotate axis = X, Angle = 27.44 deg, plus the translation
that rotating about the card's pivot implies (Y 17.60, Z -62.11). The original
reading came from a drag that never grabbed the 7 px ring; this run took its
candidate points from the rendered ring pixels themselves and 6 of 6 answered.
Rig-measured (docker snaporca-gui, Xvfb :10, llvmpipe), vertical screen shift of
the body by image correlation:
commit Translate Z 0 -> 20 : +140 px
double-click the Transform row : +0 px, and the card re-opens showing 20.00
step the re-opened card 20 -> 40 : +142 px (not +280 -> the undo term is right)
Cancel : +0 px vs the committed frame, residual 0.46
Add mode: stepping Z moves the body immediately (viewport diff bbox
200,143-1181,999); Cancel puts it back with only the status strip differing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a Wayland session the in-canvas value field never took the keyboard focus, so
after drawing a rectangle the first keystrokes went nowhere and the field had to
be clicked before a number could be typed. open() already did Show, Raise,
SetFocus on both frame and control, SelectAll, and re-asserted all of it in a
CallAfter — none of it works here, and no amount of re-asserting would: under
Wayland a client cannot focus itself, and mutter ignores gtk_window_present()
without an activation token as focus-stealing prevention. The earlier fix
recorded in this file (dropping wxFRAME_FLOAT_ON_PARENT, whose GTK _UTILITY_
hint made an xrdp session refuse focus) addressed a different compositor.
Stop needing WM focus. The canvas keeps the focus and feeds the field:
SketchInlineEditor::feed_key() types into the control directly — Enter commits,
Esc cancels, Backspace/Delete edit, digits and '-' '.' ',' are accepted, and
anything else is handed back so a stray letter cannot vanish into a numeric
field. A m_fresh flag reproduces the SelectAll semantics the field already had,
so the first digit replaces the prefill. It returns false when the control
genuinely holds the focus, so X11 keeps wx's normal routing and no character is
typed twice.
The CHAR_HOOK gates on the editor's own is_open(), NOT on inline_busy().
inline_busy is a freeze flag for the sketch tool: cleared on commit, re-set only
when the next queued field opens, with a CallAfter between them. Gating on it
left a window where the field was on screen and the flag was false — typing
worked for a rectangle's Width and not its Height.
VERIFIED at the machine on behemoth: typing the first dimension directly, with
no click, works. NOT yet confirmed: the Width -> Height handover; the is_open()
gate is diagnosed from the handover code, not observed. The hook's
SNAPORCA_KEYTRACE=1 switch logs each key with the focused widget if it needs
chasing further.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Select a line in a sketch, right-click, and every sketch verb was greyed: Trim,
Extend, Fillet, Chamfer, Offset, Mirror, the arrays, Constrain. The menu was
right and the selection was gone — two independent faults, each of which hid
the other.
FIRST, the Select-mode RightDown branch called clear_selection() before handing
the click back. Handing it back is correct: the m_right_consumed flag means "the
tool USED this right-click", and a plain right-click is not a gesture
terminator, so the offer should open. Clearing first is not: the offer describes
WHAT IS SELECTED, so wiping the selection guaranteed it could only ever describe
nothing. Deselection keeps its own gesture — left-click on empty space, a few
lines above in the same handler.
SECOND, offer_selection_kind() returned SkNone for every sketch state. The offer
table has always carried verbs for a selected line, arc, point or pair, but
nothing ever RETURNED those kinds, so fourteen rows were gated on selection bits
no code path could set. Classify the selection instead: SkLine / SkArc / SkPoint
/ Sk2Ent, via a first_selected_type() accessor on the tool and two forwarders on
the canvas.
Either fix alone measures as a failure — the classification is handed an empty
selection, or the preserved selection has no kind to match — which is why both
land together.
This is the second half of the report behind 3eb6e5d608: a user comparing the
Design tab with Onshape said "adding constraints seems to be missing"
(OrcaSlicer PR #15238). Constrain was one of the fourteen dead rows, and the
gesture that would have shown it threw the selection away first.
Verified at the machine on behemoth by Tommaso: select a line of a rectangle,
right-click, and the sketch verbs are live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On a non-English desktop the offer menu came out mixed: "Create / Add material /
Rimuovi / Fillet / chamfer / draft / Repeat / Transform / Reference / Modify",
and under Modify, "Elimina" beside "Constrain sketch".
Nothing was mistranslated. The row names went through a bare wxGetTranslation(),
which searches EVERY loaded catalogue — including wxWidgets' own wxstd. That
catalogue is loaded in the desktop's language whether or not the application has
one, and it happens to contain exactly two of our eight row names:
wxstd it: 'Remove' -> 'Rimuovi', 'Delete' -> 'Elimina'
Create, Add material, Repeat, Transform, Reference and Modify are not wx
vocabulary, so they stayed English. Two words in one language, six in another,
in the same menu — and the same trap is set for every other locale wx ships:
Supprimer, Löschen, Eliminar.
Name the domain: wxGetTranslation(s, SLIC3R_APP_KEY). These strings are now
translated by our own catalogue or not at all, which is consistent either way.
Left deliberately alone: the accelerator still renders as "Canc" rather than
"Del" on an Italian system. That is wx naming the physical key, and on an
Italian keyboard the key really is marked Canc — telling that user to press
"Del" would name a key they do not have.
Verified on the rig with LANG=it_IT: the menu now reads Remove and Delete, and
the submenu shows "Delete Canc" beside "Constrain sketch".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A user evaluating the Design tab against Onshape reported that "adding
constraints seems to be missing" — with nineteen constraint types and a solver
shipped behind it (OrcaSlicer PR #15238, exussum12).
They were not wrong about what they could see. The only ways in were an
icon-only toolbar button whose tooltip you have to hover to read, and an offer
row gated on sketch_mode with a sketch ENTITY selected, filed under "Reference".
Right after finishing a sketch — the moment you want to constrain it — neither
was in front of the user, so a shipped headline feature read as absent.
Add a model-mode row: "Constrain sketch", offered under Modify when a sketch
region is selected, routed through the new btn:constrain verb action.
on_begin_constrain() also gains a fallback to m_sel_sketch_feat. The offer
reaches it from a SkLoop selection, which carries no TREE selection, and the
function read only tree_selection() — so the new row would have answered
"Select a sketch in the tree first" about a sketch the user had visibly
selected. It now adopts the region's owning sketch and syncs the tree to match.
Verified on the rig: draw a rectangle, finish the sketch, click the region,
right-click -> Modify -> "Constrain sketch" enters Constrain mode with
"Pick 1-2 lines, then a constraint" and the Constraints (8) card listing the
sketch's inferred constraints. That path did not exist before.
Does NOT address the other half of the report: there is still no Pierce
constraint, so a sweep profile cannot be tied to its path. Tracked separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mass_properties on an open shell returned volume 96000 with an inertia diagonal
of [-4.2e7, -4.2e7, -6.9e7] for a 60x60x40 four-walled box — negative principal
moments, which no real body can have. BRepGProp::VolumeProperties integrates the
divergence theorem over whatever faces exist; on an open shell that is not a
volume at all, and the old code hid the only obvious tell by taking std::abs()
of the mass. "valid: true" then asserted the number was trustworthy.
This matters because mass_properties is what an agent or a user reaches for to
confirm a cut removed the right material. Silent nonsense there means the check
passes on garbage.
MassProps gains is_solid. For a sheet we compute surface area only — that stays
exact — and report volume 0 with the inertia left zeroed. The MCP verb returns
is_solid plus a note saying volume and inertia are not defined for an open
shell; the GUI's Mass command says "sheet body — N cm² of surface, no volume"
rather than quoting material that is not there.
Verified on the rig: the sheet now returns volume 0.0, surface_area 9600.0
(exactly 4 x 60 x 40), is_solid false. The solid controls are unchanged and
exact — a 60 mm cube reports 216000.0 and 21600.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking a face that already happened to be selected, while a Plane or Axis pick
was armed, read as a repeat pick: the click escalated to "whole body", the
capture was lost, and the card's label stayed "(none)" with nothing on screen to
explain it. On a cube it is easy to hit — the face under the cursor is often the
one already selected from the previous step.
The capture path in on_solid_picked already restores the flag for all three
tools, and reset_plane_refs()/reset_axis_refs() restore it when a pick is
abandoned — both were written as if the arm side disabled escalation. Only
CoordSys actually did (that was snaporca-u0wd). Plane and Axis never had it.
Verified on the rig: Midplane on a 60 mm cube now captures Face A (#5, top) and
Face B (#3, side) on the FIRST click each, and the resulting plane renders as
the 45-degree bisector between them, which is what a midplane of two
perpendicular faces should be. Before this, the first pick escalated to the body
and Face A stayed "(none)".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every method in CadDocument's plane dispatch falls back to offset_angle_plane()
when its references are missing. Picking Tangent and confirming with nothing
selected therefore produced an OFFSET plane, announced as "Plane added — pick it
as a sketch plane". The user asked for one construction and silently received a
different one, with nothing on screen to reveal the substitution.
Validate at the GUI boundary instead: Angle needs an edge, Midplane two faces
(and not the same face twice — that yields a plane coincident with the face,
which is well-defined and useless), Tangent a face, Two-edges two edges. Offset
and Coincident are unchanged: both are meaningful with no reference, since they
fall back to the base plane by design.
on_add_plane() now returns false when it refuses, and confirm_tool() skips
close_tool() in that case — a refusal that also threw away the picks the user
had already made would be worse than the bug.
The kernel keeps fallback_offset(): it must return something. It should just
never be reachable from a user gesture without a warning.
Verified on the Xvfb rig: Tangent with no pick refuses and creates no feature
(it created one before), the card stays open with the type preserved, Midplane
with no faces refuses with its own message, and Offset with no picks still
creates a plane as it always did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wxSpinCtrlDouble takes the mouse wheel whenever the pointer is over it. A card
taller than the panel therefore could not be scrolled past without silently
incrementing whatever field the cursor happened to be over — measured on the
rig: eight notches over the Coord Sys card turned an X hint from 1,00 into 6,00
while the panel did not move at all. The same gesture over an Extrude distance,
a Hole diameter or a mate Offset is a silent model change made by someone who
believed they were navigating, and nothing on screen reports it.
Every spin in this panel comes from one factory, so the guard goes there: an
unfocused spin hands the wheel to its parent, and the scrolled cards panel gets
it. A spin the user has deliberately focused still takes the wheel, which is the
one case where editing is what was meant.
Verified on the rig with an Extrude card: eight notches over an unfocused
Distance leave it at 10,00; clicking into it first and scrolling takes it to
13,00.
A mate between two connectors committed cleanly, recomputed without error, and
moved nothing. apply_mate was reached and computed a translation of exactly
(0, 0, 0).
The connectors were the cause, not the mate. Picking a face stores
coordsys_face and coordsys_body but left the Type combo on its default,
Point (world) — and datum_frame ignores the pick entirely for that type,
resolving the connector to coordsys_point, which is (0,0,0) unless the user
typed otherwise. Two connectors built that way share one frame, so the mate
between them is an identity transform: everything reports success and the
assembly never moves.
Capturing a face or an edge now switches the type to FaceAndDirection. Picking
a face IS the choice of a face-based frame; asking for it twice, with no hint
that the second half is required, is what made every mate a silent no-op.
Verified on the rig, two bodies with a face-based connector each:
before Extrude4 [-71.6, 402.6, 156.5] .. [-8.8, 412.6, 237.3]
after Extrude4 [-31.4, -40.4, -10.0] .. [ 31.4, 40.4, -0.0]
with the mate transform now (40.19, -196.88, 402.56) instead of (0, 0, 0).
That is the first mate in this tree that assembles anything.
Not the kernel: recompute applies mates exactly as preview does, proven by an
A/B harness over all five kinds — the two paths give identical bounding boxes.
1. A consumed loop WITH HOLES was never dropped from the sketch overlay.
sync_sketch_display compares each region's entity list against what a per-loop
extrude stored, but rebuilt the candidate from the region's OWN entities while
the extrude stores the region's entities PLUS every hole's (see
selected_loop_entities). For a plate with one bore that is 4 against 5, so the
match never fired and the extruded rectangle stayed drawn on top of the solid
it had become. Adds region_entity_indices_with_holes, which returns the same
order the extrude uses, and compares against that; a matching region now drops
its holes' entities too. Hole-less regions are unaffected.
This is also the artefact that made a correct plate-with-a-bore read as a
plate with a plug in it during rig testing.
2. The Constraints card drew its header and its first row on top of each other.
The rows and the delete buttons were parented to m_form rather than m_cards,
so they were laid out in the wrong window's coordinate space and started at
the card's top edge. Re-parented; nothing else about the card changed.
3. The mate hover ghost never appeared. The highlight handler asked for a repaint
with request_repaint(), which only queues a Refresh — and a wxMenu popup runs
its own modal loop, so the paint was not serviced until the menu closed, by
which time the ghost had been dropped. Adds DesignCanvas::repaint_now(), which
flushes the paint immediately, mirroring the m_status->Update() the status line
in the same function already needed for the same reason.
Delegated to opencode (DeepSeek V4 Pro) and reviewed by diff. Fix 1 needed an
accessor on DesignSketchTool because region_loops/RegionLoop are private — that
was outside the file list it was given, and it said so rather than working around
it.
Verified on the rig: a rectangle-plus-circle sketch extrudes to a plate with a
bore and NO overlay left on top of it, and the Constraints card shows its header
clear of eight readable rows.