mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
6a0524ea85f427c93b4b2e7d69c7948efe672d32
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6fc3c99e31 |
Mate connectors: draw the dashed pair line between the two origins
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 |
||
|
|
b4d6abc57a |
Design: draw the mate connector as a bear face, with the disc kept behind a preference
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>
|
||
|
|
555af98474 |
Mate connectors: bring the design record and the BearConnector pair into the repo
The connector work has lived outside the code since 2026-08-05, in a workspace repo with no
remote. It is the basis of a decision that now shapes the Design tab, so it belongs here.
docs/design/mate-connectors/
DESIGN_MATE_CONNECTORS.md seven CAD systems surveyed; the frame-pair model this kernel
already matches; sections 8b/8c on the glyph, and section 9's
four open decisions (D1-D4) still awaiting Tommaso.
bear.step the male, Onshape 2026-08-05T08:27Z, md5 faf228326ee3f971
BearConnector_Female*.step/.stl, BearConnector_Cutter.step
built by make_female.py FROM the real male B-rep rather than
re-modelled, so the pocket is complementary by construction
including every deliberate asymmetry. Fit measured at exactly
0.2000 mm, zero interference, mated hosts proven coplanar.
BEAR_CONNECTOR_REVIEW.md the symmetry-group result: identity 81/81 edges, mirror-x 0/81,
mirror-y 0/81, rot180Z 0/81, rot90Z 0/81, diagonal 0/81 at
0.1 mm. Trivial group, so every PARTIAL view fixes orientation.
extract_outline.py, simplify_study.py, relief_sheet.py, handedness.py, make_female.py,
trim_female.py, fit_check.py, verify_trimmed.py, coplanar_test.py + their sheets
THE DECISION THIS SUPPORTS (snaporca-x0kd): the mate connector is drawn as a simplified BEAR
FACE by default, with the standard disc + roll quadrant + Z arrow kept behind a preference.
Face orientation is hardwired perception -- a toddler reads a face's roll and verse with no
instruction -- and no abstract glyph earns that. Measured against the alternative: the disc's
gold quadrant+tick falls 89 -> 66 -> 37 -> 20 -> 3 -> 0 lit pixels as the camera drops from
47 deg to edge-on, and is a shapeless blob by 16 deg.
WHAT THE SIMPLIFICATION STUDY SETTLED (snaporca-wi3z), all measured off the real B-rep:
The eyes are load-bearing. Same outline and muzzle with the eyes removed stops reading as
a face at every size. Whatever else goes, they stay.
45 -> 22 outline vertices with no loss of read at 22 / 32 / 48 px; the muzzle reduces to
one filled triangle. Three marks plus a cheek dot.
Drawn FLAT the face fails exactly where the disc fails: in the connector's plane everything
foreshortens by sin(elevation). Rendered as its real relief instead, lit pixels at 32 px go
164 -> 210 at 16 deg and 69 -> 120 at 6 deg, and the snout ridge stands proud as a profile
rather than smearing. The glyph must be a shaded relief, not an outline.
Handedness already reads without any added mark -- 32 to 35 % of lit pixels differ from the
mirror, and re-registering by best whole-pixel translation returns offset (0,0), so it is
real shape asymmetry. But it reads only BY COMPARISON. A dot on one cheek makes it local:
34.5 / 37.0 / 36.4 %, and unlike uneven eyes (42 %) it does not read as a defect.
Tommaso's calls: it stays a bear, and handedness must read.
The scripts were repointed at the co-located male and extract_outline.py re-run from here to
prove it -- same 45 outline points, same three inner wires, same 3829.5 mm2 back plate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
799f840218 |
Thicken Surface: fill the corners of a closed-loop wall
snaporca-wm4s. Thickening the 4-walled open box (60x60 in plan, 40 tall, no caps) by 5 produced volume 29648.15 where the geometry requires (60^2-50^2)*40 = 44000 — about 67% of it. The corner material at the four vertical edges was simply absent. CAUSE. MakeThickSolidBySimple offsets each face along its own normal and sews; it never extends neighbours to meet, so wherever two faces join at an angle the corner is empty. A flat sheet has no such join and was always exact (18000.000), which is why the defect looked like a measurement artefact. WHY THE TWO EARLIER ATTEMPTS COULD NOT HAVE WORKED. Both switched to ByJoin — plain, then with Intersection/GeomAbs_Intersection — and both returned a shell, not a solid, so the body lost its volume entirely and both were reverted. That is not a parameter problem: in OCCT, BRepOffset_MakeOffset::MakeThickSolid builds a solid only inside `if (!myFaces.IsEmpty())` (BRepOffset_MakeOffset.cxx:1115). Handed an open sheet with no closing faces, it stops after the offset shell and returns it, reporting IsDone() with a non-null shape containing no TopAbs_SOLID. ByJoin hollows a CLOSED solid by removing faces; an open sheet is outside its contract. FIX. Close the sheet, then use the call that mitres: cap the free rims (ShapeAnalysis_FreeBounds -> MakeFace), sew shell+caps into a closed shell, make a solid, and hollow it inward passing the caps as the faces to remove — the caps come back off and leave the wall. Two details, each found by measurement rather than reasoning: * A shell sewn from an extruded sheet carries no guarantee of outward orientation, and MakeSolid does not fix it. Inside-out, the inward offset goes OUTWARD: measured bbox 70x70x40 and volume 339141.59, larger than its own bounding box because the result overlaps itself. A negative GProp mass is exactly that inversion, so it is the test; Reverse() on it. * A SINGLE face has no neighbour to mitre and must keep the BySimple path. It does have a free boundary, so "has free wires" is the wrong question — capping a lone face with its own rim sews a zero-thickness shell and measures 6000 against 18000. Also: IsDone() is not a success test here, since both failed attempts had it true. The code now explores for TopAbs_SOLID and refuses a shell. Tests: new case asserts 44000 with the wall's bbox at 60x60x40 (catching the inverted-orientation shape, which has the right volume nowhere near the right place), plus the flat-sheet control at 18000 that must not regress. Full kernel suite green: 2502 assertions in 187 test cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
98135cf529 |
Don't block cloud sign-in because the setup wizard is unfinished
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> |
||
|
|
7d313159df |
Fix cloud features staying off after login when the setup wizard was closed
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> |
||
|
|
8e5d0d195c |
Design: double-click a feature row to edit it, and preview Transform live
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>
|
||
|
|
f4a0bf8845 |
CAD: give Rib a shortcut, completing the pick-the-line work (snaporca-3648)
Ported from snaporca ea0e11e49d. See that commit for the acceptance measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dcbda7d42c |
CAD: deliver the picked sketch ENTITY to the panel, and point Rib at it (snaporca-3648)
Ported from snaporca 94b6b564de. See that commit for what is and is not measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
315a35e2ea |
CAD: Sweep path and Loft profiles fill from a viewport sketch pick (snaporca-ysm2, e1p item 6)
Ported from snaporca 314d30c660. See that commit for the measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7920e55413 |
CAD: give the keyboard back when the action bar hides (snaporca-ehrm)
Ported from snaporca ace5778d4c. See that commit for the measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
31548a230d |
CAD: Mirror takes its body from the viewport (snaporca-gtd3, e1p item 6)
Ported from snaporca 2516961a32. See that commit for the measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2643c778dc |
CAD: Boolean takes its two operands from the viewport (snaporca-310o, e1p item 4)
Ported from snaporca 572eb56d0e. See that commit for the measurements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dd6363f536 |
CAD: clicking a reference plane's label selects THAT plane (snaporca-uw3c)
Ported from snaporca 373ef325d8. See that commit for the full rationale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
920f0bd126 |
CAD: keep the pick trace, stop paying for it when it is off (snaporca-txp8)
Ported from snaporca 3710d34568. See that commit for the full rationale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
457610108e |
CAD tests: pin the sheet-body mass properties with the rig's own numbers (snaporca-lu27)
Ported from snaporca 35befd965c. See that commit for the full rationale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4c8c93b512 |
Design: keep the inline dimension frame mapped across queued fields (snaporca-p8uw)
Ported from snaporca e4e0e21581. See that commit for the full analysis. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d9abadc88f |
Revert "CAD: type a sketch dimension without clicking the field first"
This reverts commit
|
||
|
|
ab15f386e4 |
CAD: type a sketch dimension without clicking the field first
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> |
||
|
|
614824ce89 |
CAD: right-click in a sketch offers verbs for what is selected, instead of for nothing
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
|
||
|
|
eb52972a8e |
CAD: the offer menu speaks one language, not two
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>
|
||
|
|
3eb6e5d608 |
CAD: constraints are reachable from the offer menu, not only from a toolbar icon
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> |
||
|
|
9013f530fa |
CAD: a sheet body reports no volume, instead of a confident wrong one
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> |
||
|
|
1545fb7946 |
CAD: an armed Plane or Axis pick captures the face, instead of escalating to the body
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> |
||
|
|
f6f2edb906 |
CAD: the Plane card refuses a method it cannot build, instead of quietly building another one
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> |
||
|
|
9a5e9dfc36 |
CAD: the wheel scrolls the card panel, it does not edit the field under it
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. |
||
|
|
03fb81020e |
CAD: picking a face for a Coord Sys must also make it a face-based frame
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. |
||
|
|
c45f84edf4 |
CAD: three Design-tab fixes — consumed holed loop, constraint rows, hover ghost
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. |
||
|
|
9340d4c7dc |
CAD: the DoF readout comes back in Constrain mode
Leaving Sketch clears the "N degrees of freedom" line, which is right — it describes a sketch's constraint state and means nothing in Feature mode. But entering CONSTRAIN left it blank too, and Constrain is where the number is the whole point: the readout is fed only by a live solve, and no solve fires merely because the mode changed, so the line stayed empty until the user happened to change something. The solve callback now caches its last result and the labelling is split into apply_dof_status(), which set_ui_mode re-applies on entry to Constrain. Verified on the rig: a rectangle reports "4 degrees of freedom" in Sketch, and the same line is there after K enters Constrain. |
||
|
|
1631963ba1 |
CAD: pick tolerances scale with the face, so a narrow face is reachable
The edge and vertex tolerances were fixed at 8 and 11 px. On a face that is barely wider than that on screen — a thin plate, or any part once you zoom out — every point on it lies within the edge budget, so the pick alternated edge and whole body and the FACE level could never be reached at all. That is not just awkward: a face pick is what gives a Coord Sys its owning body, and a mate needs one, so thin parts could not be assembled. Both tolerances are now capped at a third of the face's shorter on-screen side, measured from the edge samples the picker already walks. They only ever shrink, so a face with room keeps the full budget and nothing changes for ordinary geometry; a narrow one keeps its middle for itself. Verified on the rig on a 16.7 mm-wide plate at three zoom levels including one far enough out to make the face a thin sliver: the middle reports "face 5 selected" every time, while points near the rim still take the edge. |
||
|
|
76d7dd6946 |
CAD: an armed face/edge pick must not lose its click to the body escalation
A card that asks for a face ("Click a solid FACE in the viewport") could not be
satisfied. Clicking the same sub-element twice deliberately escalates to the
whole body — right for free picking, wrong here: the user clicks the very face
the card is pointing at, the escalation turns it into a whole-body pick, and the
armed capture rejects it and leaves "(none)". Both orders failed, so a
face-based Coord Sys was reachable only by accident of ordering. That mattered
beyond the card: a mate needs a connector with an owning body, and a face or
edge pick is the only thing that sets one.
Adds DesignSketchTool::set_escalate_on_repick, off while the Plane, Axis or
CoordSys card has a pick armed and back on as soon as it is captured or
abandoned. While armed, clicking a face means "this face", which is what the
prompt already says.
Verified on the rig: arm Pick Face, click the face, and it reads "#5" on the
first click. With nothing armed the escalation still alternates whole <-> face
as before.
|
||
|
|
d8103f794b |
CAD: a mate needs B to have a body, so stop offering one when it does not
The mate palette called all five kinds viable on connectors created as Point(world), which belong to no body. Clicking one built the Mate feature and the RECOMPUTE then failed with "mate: mate_cs_b has no associated body" — the refusal arrived one step too late, after the feature was already in the tree, and the user is told about it by an error line rather than by the palette that offered the thing. mate_options now checks the same two conditions the apply path throws on: B must have a body (B is the connector whose body moves; A is the fixed reference and needs none), and that body must still resolve. Either way all five kinds go non-viable with a reason, so the offer and the kernel cannot disagree. Verified on the rig: with two Point(world) connectors, every row is now dimmed and reads "connector B is not attached to a body — a mate moves B's body". That also exercises the dimmed-with-a-reason presentation for the first time, which until now had nothing to show because every kind was always viable. Tests: a new [mate] case covering no body, a body that no longer resolves, and the revival once B is given one. One existing case needed its setup widened rather than its assertion weakened: "an unrecorded fingerprint does not make a type non-viable" built two body-less connectors, so it was asserting a side effect of the old permissiveness instead of the property it is named for. Its connectors now have a body, leaving the missing fingerprint as the only variable. Full [CadDocument] suite: 2458 assertions in 185 cases. |
||
|
|
08ef43fad8 |
CAD: the mate palette fired nothing — two menu handlers were shadowing it
Right-clicking a document with two coordinate systems builds the mate palette exactly as designed: a separator, a disabled "Mate: A -> B" header, then five rows in fixed order. Hovering a row showed no ghost and left the previous status message on screen; clicking one created nothing at all. The palette enumerated perfectly and fired nothing, which put the whole M8 mate epic out of reach from the UI. The mate handlers were never invoked. show_offer_menu binds two pairs of handlers to the SAME wxMenu: the mate pair (by mate_base, at base + 500) and the generic verb pair. wxWidgets pushes dynamic entries to the FRONT of the handler list, so the pair bound LAST runs FIRST for every id — and the generic pair was last. A mate id lands outside the verb table's range, so both generic lambdas returned early WITHOUT e.Skip(), which wx reads as "handled", and the mate handlers behind them never saw the event. Binds the two generic handlers to the verb id range [base, base + 499] so each half only sees its own ids regardless of bind order. Verified on the rig with a purpose-built assembly (two bodies 60 mm apart, a Coord Sys on each): before, clicking Fastened produced no feature and no status change; after, it produces a Mate feature. The verb rows keep working — the same run created both coordinate systems through Reference > Coord Sys. Note the mate then fails its recompute with "mate: mate_cs_b has no associated body", because a Point(world) coordinate system belongs to no body while the palette still advertises all five kinds as viable. That is a separate defect and is filed; this commit is about the palette being reachable at all. |
||
|
|
7b953d564d |
CAD: a project whose model is a recipe is not an empty file
Reopening a saved CAD design ended on a modal "The file does not contain any geometry data." warning. A CAD project legitimately carries no mesh — the model lives in the feature tree (Metadata/SnapOrca_cad.bin) until Commit to Plate — so the warning is false for one, and it is the last thing the user sees after opening a design they spent an hour on. It reads as "your work is gone" at the exact moment the recipe HAS just loaded and the Design tab is about to rehydrate it, and the main window sits disabled behind the dialog until it is dismissed. Counts a non-empty model.cad_recipe as geometry. Verified on the rig: save without Commit to Plate, reopen, and the Design tab comes up with Sketch1 -> Extrude2 -> Body 1 editable, with no dialog at all. |
||
|
|
5889f6640f |
CAD: extrude a sketch region with its holes, and stop crashing at startup
A rectangle with a circle inside it, drawn in ONE sketch, could not be extruded to a plate with a bore from the GUI. Five defects were in the way. Each was found by driving the app on a headless rig and measuring the result — the code reads correctly at every one of these points, which is why they survived. 1. Wire orientation (kernel). SketchEngine::wires_to_face added every hole as wires[i].Reversed(), which is only right when the sketch happens to wind both loops the same way. A circle drawn clockwise inside a counter-clockwise rectangle came out matching the outer boundary, OCCT swept it as a SECOND contour, and the prism was the plate with its bore filled and the disc's volume counted twice. Measured: bbox 67.17 x 219.67 x 10 with volume 152088 mm3 against a solid box of 147542 — a body larger than its own bounding box, which is the signature. Holes are now added as-is and ShapeFix_Face::FixOrientation() classifies them; that is winding-independent and is the idiom make_extrude_regions already used for imported glyphs, which is why holed TEXT always extruded correctly while a holed SKETCH never did. After the fix: 142996 mm3, implied bore radius 12.03 mm against the circle drawn. 2. The live-sketch click threw the picked region away. region_at() served only as a yes/no gate and on_face_selected() carried no argument, so Extrude fell back to whichever loop the resolver found first — clicking the material of a plate-with-a-hole extruded the disc. The region is now carried through, and DesignPanel also hands it to the tool with set_loop_pick(), AFTER open_tool() because that re-derives selection state, since extrude_uses_loop() reads selected_loop_entities() and that lives on the tool. 3. region_at() had no hole awareness and no innermost preference: it returned the first polygon containing the point. It now skips a region when the point lies inside one of that region's holes, and picks the smallest containing loop, so a click in the bore selects the disc and a click on the material selects the plate. 4. Startup segfault. DesignCanvas::request_repaint probed the GL backend via OpenGLManager::get_gl_info().get_renderer() before the canvas had initialised GL — glGetString with no context current and, before init_opengl(), no loaded function pointers. Anything that asked for a repaint while the panel was still being built landed there, with no window and nothing in the log. It now bails at the top on !is_initialized() and asks for a Refresh instead. Note the crash was in the PROBE, not in render(), which already guards itself. 5. A holed sketch on a plane whose normal points -Z came out as the full box PLUS a disc — 220274 mm3 where 163726 was due (192000 + 28274). wires_to_face took a SketchPlane parameter it never used and let OCCT infer a surface from the outer wire; when the inferred normal disagreed with the sketch's, the hole classification produced no hole. Every face is now built on the sketch's own gp_Pln. Also in this change, from the same rig session: - A right-click that only clears the sketch selection no longer reports itself as consumed, so it stops suppressing the offer menu. With any geometry in a live sketch there was no menu route left to add a second entity. - Escape no longer discards a live sketch that holds drawn geometry; it says so and keeps the work (live_sketch_has_work()). - The holed-region fill is an even-odd scanline instead of a keyhole bridge, so no corridor triangle leaks from the bore to the nearest corner. - Cyan is reserved for the selection: an unselected region no longer wears a shade one step off the selected one. - The origin planes follow the mode, so pressing Sketch on a document that already has a body offers them again instead of naming a plane you cannot see. Tests: three [holes] cases over add_extrude_entities asserting the plate-with-bore volume, solid and face counts on both a +Z and a -Z sketch plane, and the by-name refusal of two disjoint regions. Full [CadDocument] suite green. |
||
|
|
e27a44e6ef | Merge remote-tracking branch 'prfork/cad-mainline' into cad-mainline | ||
|
|
57b42bc059 |
Offer the origin planes while choosing a sketch plane, not only before the first body
Delete a sketch on a document that still has a body and you could not start a new one. Pressing Sketch said "click a face or a reference plane in the viewport" — while update_reference_planes had already called clear_base_pick, because a body existed. The instruction named something that was no longer there, and short of finding a face to click there was no way back into sketching at all. The planes are now offered when there is no solid yet OR while the UI is in Sketch mode, and set_ui_mode refreshes them so they appear the moment you press Sketch rather than at the next tree rebuild — which is not an event that pressing Sketch causes. Deliberately not always-on. m_dbp_active both RENDERS and picks, so leaving it set would float three translucent planes over every finished model. Tying them to the mode shows them exactly when they are the thing being chosen and takes them away again on Finish. Safe against stealing clicks: a base-plane pick is the last resort in on_mouse, firing only on a click that hit no geometry, so solids and committed sketches still win where they overlap. Found on the rig by Tommaso: "if i remove a sketch, i cannot create sketches anymore". |
||
|
|
81876a6ce6 |
Sketch regions understand holes, so the plate with the hole can be selected and extruded
A rectangle with a circle inside it extruded to a plain box. Tommaso reported it exactly right on the first attempt — "no intersection selectable, hence no plate with hole" — and it was a causal chain, not a guess. Two things were wrong and they compounded. region_loops() returned N independent filled polygons with no notion of nesting, so the only selectable things were the rectangle alone and the circle alone. The region a user actually wants — the bounded area WITH its hole — did not exist to be pointed at. Worse, the first polygon containing the click won, so clicking inside the circle selected the rectangle. And extrude_uses_loop() hands selected_loop_entities() to add_extrude_entities, which copied only that one loop's entities into the feature. So the circle never reached the kernel, build_sketch_face saw a single loop, and the multi-loop path added in 5c4ced91e7 never ran. Proven from his saved project: Sketch1 held 5 entities, Extrude2 held 4, and the committed mesh was 8 vertices — a box of 260.40 x 220.91 x 10.00. Eight vertices cannot describe a bore. A RegionLoop now carries the loops nested inside it. Containment is decided by testing one vertex, which is sufficient because loops in a well-formed sketch do not cross, and each loop is assigned to the SMALLEST loop containing it so a hole belongs to the region that actually bounds it. Picking respects holes: a click in the plate selects the plate, a click in the bore selects the disc. The selection hands over the region's own entities plus its holes', which is what finally reaches the kernel. The highlight lights the holes with their region, because it has to show what will be extruded. The status line said "Loop selected"; it now says "Region selected". What is selected is a bounded area that may contain holes, not a single closed curve — the old wording described the old, broken behaviour. snaporca-txp8, and it is what makes snaporca-88v reachable from the GUI at all: the kernel could build the holed face all along (verified on his own recipe: 2 closed loops, wires_to_face OK, area 74812.119 mm2), but nothing could ask it to. Reviewed and compiled (RC=0). NOT exercised — the rig check is the point. |
||
|
|
b80f4e3036 |
Persist the CAD recipe on every save, not only on Commit to Plate
Modelling in the Design tab and pressing Ctrl+S saved a project with no feature history at all, and the app reported success. Found on the rig: a project saved after drawing a rectangle and a circle contained twelve archive entries, none of them Metadata/SnapOrca_cad.bin, and a 3dmodel.model with zero vertices. plater->model().cad_recipe was assigned in exactly one place — on_commit(), immediately after load_mesh_object. The 3MF exporter was never at fault: it faithfully wrote whatever the Model held, and on a save that had not gone through Commit to Plate that string had never been set. The recipe reached the Model only as a side effect of a different user action. It now tracks the document instead. sync_recipe_to_model() is called after a successful recompute, after tree edits (deletes, reorders and suppressions bypass recompute_guarded), and from on_commit, which delegates rather than repeating the rule. Only on success — a failed recompute leaves the document mid-edit, and persisting that would save a model the user never had. An empty document still clears it, so a non-CAD project carries no stale recipe. Doing it here rather than in the save path is deliberate: Ctrl+S, Save As, autosave and crash recovery all read model.cad_recipe, so keeping it current after each change makes every one of them correct at once, instead of teaching each save path to ask the Design tab. Commit to Plate means "send this to the slicer" — making saving depend on it was the bug, not the cure. Cost is one serialization per recompute, tens of KB against an OCCT rebuild that has just run. Why nothing caught it: the kernel round-trip tests serialize a CadDocument directly, and the 3MF tests exercise the exporter with a recipe already present. Neither can observe that the GUI never populates it, and every save in testing happened to follow a Commit to Plate. snaporca-vjk5. Reviewed and compiled (RC=0); persistence NOT yet confirmed on the rig — that check is the reason the issue stays open. |
||
|
|
56eebe3398 |
kernel-test: stop configuring the GUI, which the kernel suite never needed
This script builds only libslic3r_tests, which links libslic3r and no GUI code — but cmake still processed the whole if(SLIC3R_GUI) block and every find_package inside it, so the kernel suite silently depended on the GUI's dependency set. That came due the moment upstream added wxInspector as a REQUIRED find_package: the orcacad-deps image predates it, so configure died pointing at src/CMakeLists.txt:92 with nothing about the kernel having changed. Turning the block off is not a workaround for that one dependency — it is the suite finally declaring what it actually needs, so the next GUI-side dependency added upstream cannot break it either. Surfaced by taking SoftFever's merge of main into the PR branch. |
||
|
|
358c331cc6 |
Merge SoftFever's main-into-cad-mainline update
He merged upstream main into the PR branch himself on 2026-08-13. Taking it into the local branch rather than force-pushing over it: the fork copy is what PR #15238 shows, and discarding a maintainer's merge to make my own push fast-forward would be both rude and a loss of 130 upstream commits. Brings the branch far closer to main than the 2026-07-24 merge-base the PR body describes, which is most of what snaporca-36u9 was filed for. |
||
|
|
0e7fcb3daf |
Recipe v5: length-frame every feature, so the format stops orphaning projects
Every version bump so far has permanently orphaned every project saved before it. deserialize_recipe refused anything that was not exactly the current version, and with no migration path v2 and v3 projects are unopenable today — the 3MF still carries the mesh, so the user gets a frozen solid and no feature history, which is the whole point of the subsystem silently absent. The cause was the shape of the data, not the gate. save/load is one flat symmetric list of ~90 fields with no framing, so a reader has no way to know where a feature ends unless it agrees on every field. Each feature is now written as its own cereal stream behind a length prefix, and the same few lines handle both directions of mismatch. Older file, newer build: the sub-stream ends early, the read throws, and the fields already assigned are kept while the rest default — cereal assigns sequentially, so a mid-list throw leaves the earlier fields set, and that is what makes this work. Newer file, older build: the sub-stream holds more bytes than the reader knows; it reads what it knows and stops, and the outer stream is untouched because the length prefix was consumed in full. A field a project predates is not a corrupt project, so neither case is an error. v4 keeps its own pre-framing flat path and opens exactly as before — cad_recipe_v4.bin is untouched and now serves as the witness for that. v2 and v3 stay refused, by name: their field lists no longer exist in this code. This fixes the future, not the past, and the comment says so rather than implying otherwise. From here a new field only needs appending to save/load — no bump, no orphaned projects. That removes the cost that had blocked snaporca-44m and snaporca-dgv. The helix round-trip test was reading the blob back flat, reaching into the format instead of through it; framing necessarily breaks that, so it now goes through deserialize_recipe, which is a stronger assertion than it made before. Every field check it carried is unchanged. Tests: four new [CadDocument][recipe] cases, including the one the change exists for — a deliberately truncated feature blob must LOAD, keeping what it could read. Suite 177 -> 181 cases, 2366 -> 2411 assertions. snaporca-2txy. |
||
|
|
4b3ff99004 |
Project load: say WHY the CAD model could not be restored
deserialize_recipe distinguishes three cases that matter very differently to the person reading the message — saved by a NEWER build, saved by an OLDER one, or genuinely unreadable — and names the version in each. load_recipe threw all of that away and printed one generic sentence, so the user could not tell "update SnapOrca" from "your file is damaged", and had no way to find out. Same error-loss class as the 31 McpControl sites fixed in 1de72de9ed: the message existed, it was simply not passed on. The generic sentence stays as the fallback for the case where the kernel really has nothing to say. This does not make old projects loadable — that is snaporca-2txy, which the audit behind this change opened. It only stops the reason being withheld. snaporca-2txy (partial). Reviewed and compiled (RC=0), not exercised. |
||
|
|
7245415af7 |
Sketch: a profile may hold more than one closed loop — a plate with a hole extrudes
entities_to_wire handled exactly two shapes of sketch: one lone Circle/Ellipse, or
any number of Line/Arc/EllipseArc/BSpline pushed into a single MakeWire. Everything
else fell off the end as a null wire, so a circle drawn inside a rectangle — the
most ordinary thing in this whole program — refused with "not supported yet". Two
separate closed polygons were quietly worse: both went into one MakeWire, which
does not mean "two loops" to OCCT.
entities_to_wires now returns one wire per loop. A Circle or Ellipse is a loop on
its own; chain entities are grouped by shared endpoints (union-find, 1e-6 in sketch
coordinates), and an open chain still comes back as a wire because a sweep path is
legitimately open. It is all-or-nothing: one loop that fails to build poisons the
whole result, because a partial profile would extrude a shape the user did not draw
— the failure 2e6a8f9e91 was written to stop.
entities_to_wire survives as a two-line wrapper returning the single wire when
there is exactly one loop and a null wire otherwise, so all nine of its call sites
keep their exact contract and Revolve/Sweep/Loft/Surface* are untouched. What a
holed profile means for each of those is a separate question.
wires_to_face takes the largest-area loop as the outer boundary and adds the rest
reversed, which is how OCCT is told a wire is a hole. Containment is CHECKED with
BRepClass_FaceClassifier, not assumed: a loop outside the largest one is a second
island, and one sketch producing several solids is a much bigger feature, so it is
refused by name ("two disjoint regions") rather than guessed at.
Only the Extrude case consumes the new face. Tapered extrudes of a holed profile
are refused — offsetting inner loops has to go the opposite way — and the guard
counts wires on the face already built rather than rebuilding every wire to ask how
many there are, which is also the more honest test: what matters is the profile
being extruded.
Tests: six new [CadDocument][sketchwire] cases, proved by VOLUME rather than by not
throwing — plate-with-hole, two holes, and two regression guards that a lone circle
and a lone polygon extrude exactly as before. Suite 177 cases / 2366 assertions.
No serialized field, recipe version untouched, golden fixtures unchanged.
snaporca-88v.
|
||
|
|
ad8b5a73fe |
Hover pre-highlight: show what a click would take, before it is taken
Third and last piece of the selection model. The other two turned out to be built already — the rubber band is pick_bodies_in_rectangle and vertex picking is SolidSel::Vertex with its camera-facing square, both live — so this closes what the issue actually still described. Vertex beats edge beats face is a rule the user cannot see until after they have committed to a click. Showing the outcome under the pointer is what makes the precedence learnable at all, and is the charter's L5 read honestly: one click, one visible change means the change has to be predictable BEFORE the click, not only explicable after it. The resolution is now one function, resolve_solid_pick, const and writing only into its out-parameter. The click applies it and then runs its escalation unchanged; the hover applies nothing. Split this way the promise cannot drift from the act — a second implementation of "what is under the cursor" would eventually disagree with the first, and the disagreement would look like a picking bug rather than a duplication one. Rendering is likewise one function called twice. The pre-highlight draws first so the committed selection paints over it, and is suppressed entirely when the two are the same thing: two coats of the same colour reads as a rendering fault, and a promise about a click that would change nothing is not worth making. It is desaturated toward white rather than given its own hue — a distinct colour would read as a distinct KIND of selection, when it is the same selection one moment earlier. Two things that would have been silent bugs. The edge ribbon and the vertex square render with GL_BLEND off, so an alpha below 1 there is ignored; those two are quietened by a muted rgb and only the blended face fill takes the alpha multiplier. And the pre-highlight is cleared in clear_solid_selection, because it names a face by an index into a shape a recompute has just rebuilt — left behind, it would keep glowing on whatever now sits at that index, a real entity but not the one meant. Hover runs on plain motion only, with no button down and no band running: during a drag the pointer is doing something else and a promise about clicking would be a lie. It returns false so the event still reaches the camera — it asks for a repaint, it does not consume the gesture. snaporca-9xw. Reviewed and compiled (RC=0), not exercised. |
||
|
|
498c7ff8d1 |
Pattern/Cut/Boolean: grey the button when there is no body, and say why
Tommaso reported the array controls as missing. They were not — Shift+N opens a Pattern card with every control correct — but the report was fair. With no body the button accepts the click, opens nothing, and writes its refusal somewhere other than where the click happened. From the user's seat that is indistinguishable from a dead button, and the icon is one unlabelled glyph among fourteen, which is how I mis-clicked it into Section view while reproducing this. A control that cannot act should look like it cannot act, before it is pressed. The three FEATURE buttons carrying a body-count guard — Pattern and Cut at one body, Boolean at two — are now greyed below their threshold with a tooltip naming what is missing. Only those three. The same guard shape also appears on rows INSIDE the flyouts, and those stay live: a drawer holds sketch-only entries too, so disabling the drawer would hide tools that are perfectly usable. The keyboard shortcuts keep running the guarded action rather than being gated — a key press has no greyed-out state to see, so the sentence is the only feedback there is. Re-evaluated in feed_bodies(), before its viewport early-return since this is about the toolbar and not the canvas, and once after the toolbar is built: an empty document is the state the bug was reported in and feed_bodies has not run yet on a fresh tab. snaporca-o9j. Reviewed and compiled (RC=0), not exercised. |
||
|
|
13922a52b6 |
Design status: clear the DoF line on leaving sketch mode, and wrap the HUD chip
Two independent leftovers, both in the same status area. snaporca-752: the "N degrees of freedom" line described a sketch's constraint state and stayed on screen after Confirm, Cancel and the Escape downgrade, in Feature mode where it means nothing — visible in every Feature-mode screenshot of the 2026-07-27 sweep. Cleared in set_ui_mode rather than at those three exits, because that is the one place all of them pass through and a fourth exit added later would otherwise reintroduce it. Constrain mode keeps the readout: that is where the number is the whole point. snaporca-8cc: moving the status out of the panel and into the viewport HUD removed the clipping, but not the underlying problem. The chip is a top-level popup that Fit()s to its text, so a long sentence grew past the right edge of the canvas and hung over the window instead of being cut off inside it — the same silent length limit wearing a different hat. The label now wraps to the room actually available (canvas width minus the view-cube inset), which is what makes the earlier promise that "a sentence can be a sentence" true at 1366 as well as at 1920. SetLabel + Wrap + Fit are now one function called from both the text change and the placement. Wrap() rewrites the label it is handed, so it has to follow a fresh SetLabel every time, and the placement path runs on resize — a chip wrapped for the old width either overhangs a narrowed canvas or wastes a widened one. The left inset is one constant now because the wrap width and the anchor have to agree, or the chip wraps to a width it is not then given. snaporca-752, snaporca-8cc. Reviewed and compiled (RC=0), not exercised. |
||
|
|
8737ff701e |
i18n: drop regenerated catalogues from the PR branch
The .pot, the Italian .po and list.txt are build product: 27,314 of the added lines in this branch were regenerated catalogues rather than code, and a reviewer running git diff --shortstat met that number before anything else. Restored to the merge-base so their diff is zero; they regenerate from source with scripts/run_gettext.sh whenever the maintainers want them refreshed. The Romanian catalogue goes with them, for a different reason: it is a complete new translation and deserves its own PR rather than riding along inside a CAD feature, where nobody qualified to review it would think to look. Nothing here changes what the Design tab does. The strings are still marked for translation in the sources; only the generated catalogues are out. |
||
|
|
1a6252c88c |
Hole/Thread re-edit: restore the face latch from the feature, not from the last pick
m_hole_on_face and m_thread_on_face are cleared only by their tool's flyout and by their plane combobox, so after any on-face hole or thread the flag stays true for the rest of the session. load_feature_into_dialog restored the stored plane into the dropdown but never touched the latch, so re-editing from the feature tree ignored the plane it had just restored: hole_plane() returned the still-latched face plane, which may belong to a different face, a different body, or a body since rebuilt. Silent until snaporca-200 added the "On face" row, which then read as a confidently wrong answer rather than as nothing. The latch is now rebuilt from the stored feature, which is the only source that describes THIS hole. Not from the dropdown row: index_from_plane snaps an arbitrary face plane to the nearest XY/XZ/YZ, so driving the re-edit from the row would MOVE a hole drilled on a slanted or offset face — that was the reason the other candidate fix was rejected. is_base_plane() decides which of the two a stored plane is. It compares the origin as well as the axes (a plane parallel to XY but 12 mm up snaps to row 0 and would come back at z=0), and adds modeling_origin before comparing, because hole_plane() and thread_plane() add it to the dropdown plane before the feature stores it — a document with a shifted origin would otherwise mistake every dropdown hole for a face pick. Vector norms, not isApprox, which is relative to magnitude and useless against the zero origin. The face's (u,v) extent is not serialized, so m_hole_has_bounds is cleared: the gizmo's footprint clamp goes unbounded, which is honest, where another face's bounds are not. The label says which body the face belongs to instead of a face number the feature does not carry; "(none — uses Hole plane)" is the one thing that is definitely false there. snaporca-uif9. Reviewed and compiled (RC=0), not exercised. |
||
|
|
6b3711fb08 |
Mate preview: hover a mate row and see the assembly move, commit nothing (G3)
refresh_preview() listed Tool::Mate among the features that produce no solid and cleared the ghost, with a comment saying a mate has no 3D ghost. The kernel never agreed: preview() routes a Mate candidate through apply_mate on a throwaway copy of the bodies, and build_candidate already filled the mate fields. That one early return was the whole of epic gap G3. A mate makes no NEW geometry but it MOVES a body, and the moved assembly is the ghost worth showing. Both the Mate card and the offer's mate palette now show it: hovering a palette row previews that kind, leaving the row drops it, and choosing one commits. Nothing is written to the document until the click. The committed bodies are hidden while the ghost is up — it is the whole assembly in its post-mate pose, not an added lump, so leaving them visible would draw the mated body twice and z-fight every other body against its own copy. Same reason Dressup and Draft hide them. Cleanup is after PopupMenu rather than on a close event: PopupMenu is modal, so by then the menu is gone and any command it raised has run. A flag distinguishes a ghost this menu put up from a preview that was already on screen. snaporca-b4sp. Reviewed and compiled (RC=0), not exercised. |
||
|
|
8e15ad23e2 |
Mate palette: five types on the offer, dimmed with the reason, naming the pair
snaporca-lukg part B. The issue describes building a contextual viewport palette with a stable icon set, non-viable options dimmed and explained rather than hidden, and edge-aware placement. show_offer_menu() already does all three — its dead-row branch appends a disabled row with " — " and a reason, and wxMenu places itself against the screen edge. So this is not a new widget. It is one section added to that menu, fed by mate_options(). The header row names the pair: "Mate: A → B". That is epic gap G4 — the mate card is abstract dropdowns and never says which body moves. B is the connector on the body that MOVES, so B is the arrow's destination; the parameter order invites the opposite guess, which is why it is commented at the point of use. Five rows in one loop over the kernel's result, never reordered and never filtered. The palette addresses rows by position, so a shorter list would move every row below it — which is the whole argument for dimming instead of hiding. The pair comes from the Mate card's combos when that card is open, so the offer and the card cannot disagree about what they are acting on; otherwise the first two enabled connectors, which is defensible only because the header names them. An offer acting on an unnamed pair would be worse than no offer. REVIEW CATCH: the five type names arrived as an array indexed by kind, read as _L(table[i]). That compiles and is silently untranslatable — _L is a gettext macro and the extractor scans SOURCE for literals, so five strings would have shipped that are never in the catalogue. These names appear nowhere else in the tree, so that would have been their only occurrence. Now a switch of literal _L() calls. G3 INVESTIGATED, NOT BUILT, as specified. preview() DOES handle a Mate candidate: it copies the committed bodies to a temporary and routes the candidate through apply_mate on that copy, committing nothing, and build_candidate already fills the mate fields. The only blocker to a hover preview is refresh_preview()'s Tool::Mate early return, which clears the preview on the belief that a mate has no ghost. Nothing in the kernel refuses it. Filed rather than built. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-lukg. |
||
|
|
6b3642fa53 |
Mate viability: which of the five apply, and why the others do not
snaporca-lukg wants a palette offering all five mate types with the non-viable
ones DIMMED AND EXPLAINED rather than hidden — its reasoning being that a menu
changing shape between invocations destroys the motor memory experts rely on.
That needs an answer this document could not give. This is that answer, and
nothing else: mate_options(cs_a, cs_b) returns five MateOption{kind, viable,
reason}, always five, always in kind order, never filtered.
The geometry test rides on the fingerprint added for snaporca-kqih, which is why
it costs no new serialized field: coordsys_face_kind already records the surface
type. Revolute and Cylindrical need a cylindrical face at both ends because they
need an axis to turn about; Planar needs flat faces; Fastened and Slider
constrain frames rather than surfaces, so no geometry test applies to them.
UNKNOWN IS PERMISSIVE. A fingerprint of -1 means PointWorld or a connector that
has not resolved yet, and it does NOT make a type non-viable. Refusing on missing
information is the false-alarm behaviour that gets a whole feature ignored — the
same reasoning already recorded on kqih for the drift warning, applied again
because it is the same trade.
Reasons name WHICH connector is the problem when only one is. "needs a
cylindrical face at both ends" tells the user what the rule is; "connector A is
on a flat face" tells them where to look, and the second half is the one that
saves the time.
The stability contract has its own test, asserting five entries in kind order
even for a completely invalid pair. That matters more than any individual
verdict: the palette addresses rows by position, so a shorter list would move
every row below it.
Golden fixture unchanged — this is a pure query. Suite 167 -> 171 cases,
2284 -> 2348 assertions, green. snaporca-lukg part A; the palette is part B.
|
||
|
|
a3398c6609 |
MCP: let a caller find out its face/edge ids went stale
snaporca-rgbj measured the damage: four chamfers on a box remove 0.400/0.397/0.397/0.395 mm3 when each id is re-read, and 0.400/0.008/0.397/0.280 when the four ids are captured up front. The kernel is right in both runs — the second one asks for the wrong edges. Neither errors, because a stale id still resolves to a real edge, just not the one that was measured. That makes it an API problem rather than a script bug. Reading the scene once and then issuing several operations is the natural way to drive a socket, it is what every agent will write, and it produced silently wrong geometry with nothing anywhere reporting it. CadDocument::topo_generation is bumped where the bodies are replaced — the single line in recompute() where the face and edge maps actually change, so a feature type added later cannot forget to bump it, which a per-mutator counter would invite. describe_scene and query_topology return it. A caller may pass it back as "generation" on any call, and a mismatch is refused with a message that says what to do about it. Two deliberate choices: OPTIONAL, not mandatory. Every existing script keeps working unchanged; passing the generation is what buys the guarantee. Making it required would break every caller to fix a mistake only some of them make. CHECKED AT THE DISPATCHER, not in each handler. One site covers fillet, chamfer, shell, draft, coordsys, thicken, cut, project, delete_face and everything added after them. A per-handler check is a list that goes stale the first time someone adds a method in a hurry. Not serialized: an id means something only within the run that produced it, so persisting the counter would promise a stability the ids themselves do not have. No recipe version change. describe_tools now carries an id_lifetime note, because the guard only helps a caller who knows to ask for it. Kernel suite 167 cases / 2277 assertions green; libslic3r_gui builds. The guard itself is NOT exercised — it needs the socket, so it is on snaporca-bdco. snaporca-o1l2. |
||
|
|
9125e0b4f8 |
Connector face drift: warn without crying wolf — and bump the recipe version
kqih option (c). A FaceAndDirection connector stores a global face index, and an upstream edit can renumber faces so the index silently names a different one. The DANGLING case already threw; this is the in-range-but-wrong case, which nothing detected. Fingerprint the face on first resolve, compare afterwards, and report a mismatch into mate_conflicts — the channel that already marks the tree row — never as an error. A drift warning must not abort the recompute, because the alternative makes a legitimate Draft on a mated face fatal. WHAT THE FINGERPRINT IS, AND WHAT IT IS NOT. Surface type plus edge count. Not centroid or area: legitimate parametric edits move and resize faces, which is the entire point of the model, so either would fire on every dimension change. Not the normal, which is the tempting one — Draft deliberately tilts a face and Transform reorients a body, both legitimate. Type and edge count survive rigid motion, tilting and resizing, and catch the case that actually happens: a planar index sliding onto a fillet's cylindrical face after a dress-up inserts faces. The accepted cost is that a slide between two planar 4-edge faces is invisible. A partial detector that never cries wolf beats a total one that does, because a false alarm on a valid connector teaches people to ignore the warning. Connectors with no fingerprint record one on first recompute, so old recipes self-heal and both writers (DesignPanel, McpControl) get it without changing. THE VERSION BUMP IS THE IMPORTANT HALF. The task was specified with "do not change the recipe version" — that was wrong, and the rule is written in the header three lines above the constant: bump whenever save/load gains a field. deserialize_recipe() gates on v == VERSION and then reads a FLAT symmetric field list. A v3 blob under a v3 build that has grown two fields passes the gate and reads two ints past the end of every connector, into the next feature's bytes. That is silent corruption of a saved project, which is worse than any load error. Now v4, and v3 gets the existing clean refusal. cad_recipe_v3.bin is KEPT, unregenerated, with a test asserting it is refused and that nothing half-read is left behind. It is the only artefact that can prove the gate works, because it was written by an older build — regenerating it with today's code would destroy the evidence, which the test says in as many words. Suite 163 -> 167 cases, 2248 -> 2277 assertions, green. Fixture v4 34928 bytes. snaporca-kqih. |
||
|
|
13c702bed3 |
Chamfer drift is the driver's, not the kernel's — measured, not argued
Two tests that separate a hypothesis nobody had tested. The socket showed four chamfers on a filleted rim removing 29.6 / 20.0 / 10.3 / 7.5 mm3, falling steadily. That could be the chamfer maths degenerating on a filleted rim, or it could be how the driver captured its edge ids. Those have completely different fixes, so the first job was to find out which. dressup_edge is a global index into TopExp::MapShapes(shape, TopAbs_EDGE), resolved against the body AS IT STANDS at that feature's position, and every dress-up rewrites that map. So the two usage patterns are: ids re-read after each chamfer: 0.400, 0.397, 0.397, 0.395 mm3 (max/min 1.01) four ids captured up-front: 0.400, 0.008, 0.397, 0.280 mm3 (max/min ~48) The kernel chamfers uniformly when handed a fresh id. It degrades only when handed ids snapshot against an earlier shape — and the second chamfer's stale id landed on a nearly-consumed edge and cut two percent of what was asked. That is the accumulating-drift signature the socket showed. Conclusion: driver artefact. apply_chamfer and OCCT are not at fault. The part that makes this worth a test rather than a note: IT DOES NOT THROW. ok=1, error empty. A stale id still resolves to a valid edge — just the wrong one — so nothing anywhere reports it. Silent wrong geometry, which is the class this project does not tolerate, reachable by any caller that reads the scene once and then issues several dress-ups. Test 2 asserts the non-uniformity as CURRENT BEHAVIOUR and says so in the code: it documents a defect, it does not bless one. When the driver contract is fixed it should be rewritten, not deleted. Suite 161 -> 163 cases, 2217 -> 2248 assertions, green. Tests only, no production code. snaporca-rgbj. |
||
|
|
488c94e957 |
SurfaceOffset and ThickenSurface: the arrow stands on a face, the tool still takes the sheet
These were the last tools from the charter audit with no handle at all, and the issue filed against them offered three options, all of which changed the tool. Reading on_add_surface_offset() dissolved the question instead. The premise was that a distance handle needs a frame, a sheet body has no single normal, and therefore the tool must start demanding a face. But the face was never needed for the OPERATION — only for the ARROW. Both tools still offset or thicken the entire sheet named in the combo. The picked face only says where to stand the handle. So the arrow appears whenever a face of that sheet is under selection, and its absence costs nothing: the card alone works exactly as before. Purely additive — no existing flow changes, and there is no new precondition for the user to learn. That is strictly better than any of (a) anchor on the first face and be wrong on a curved sheet, (b) sample a normal at the bbox centre and be arbitrary on a folded one, or (c) require a face pick and change what the tool demands. The arrow is refused when the picked face belongs to a DIFFERENT body than the sheet in the combo. An arrow standing on one body while the tool acts on another would name the wrong thing, which is worse than no arrow. ThickenSurface was not on the audit's list — it is a distinct tool from Thicken, with its own card and its own sheet-body combo, and it has exactly the same shape. Fixing one and not the other would have left the same gap under a different name. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-9fel. |
||
|
|
416e7f7321 |
Mate conflicts: mark the row that carries them, and name the way out
detect_mate_conflicts() has been filling m_doc.mate_conflicts on every recompute
since the kernel half landed, and nothing read it. The diagnostics existed and
were invisible — a conflicting assembly looked exactly like a working one.
The tree row is where they go, because the tree is where the user is already
looking for which feature to change. Three states, in precedence order:
disabled -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it
must read as suppressed rather than keep shouting about it.
conflict -> warn.
otherwise -> normal.
Selecting a marked row puts the reason on the status line — "Mate3 already
positions Body 2", the cycle, the self-mate — and names the eye as the way to
suppress it. A message that describes a problem with no action is a message that
gets ignored; the action here is already one click away on the row just selected.
Deliberately NOT a modal, and deliberately not treated as a document error. The
document still evaluates with a conflict present: the mate graph merely has more
than one answer for a body, and which one wins is the thing the user needs to
see. Blocking the loop to say so would interrupt without helping.
The dimming of non-involved bodies from the original UX proposal is still not
implemented, on purpose: under transform composition a failure mid-chain
propagates, so "not involved" is not a well-defined set, and dimming the wrong
bodies would hide the context needed to understand the conflict.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bioq.
|
||
|
|
7311cb12cb |
Body-focus picking: fail open, and keep the combo and the viewport as one state
Two defects found by auditing the body-focus x-ray path, which shipped compiled but never exercised. Neither is reachable from the happy path its test plan walks, which is why compiling it proved nothing. 1. A STALE FOCUS KILLED THE VIEWPORT. The focus is a body INDEX held by the panel across recomputes, so it outlives the body it names: delete a body and the stored index can point past the end. body_pickable() then rejected EVERY body, because none of them equals an index that no longer exists — a viewport that silently accepts no clicks at all, with nothing on screen saying why. Out of range now means no restriction. Fail open, never dead. 2. THE COMBO AND THE FOCUS COULD DISAGREE. refresh_cs_body_choice() rebuilds the Body combo and, when the body list shrank, silently reset the selection to "(all)" — while the viewport stayed focused on the old index. Every other body kept its 25% alpha and picking stayed restricted to a body that might be gone. That is the exact mirror of the open_tool ordering bug this feature already fixed once: that one showed "Body N" over an opaque scene, this one shows "(all)" over a dimmed one. They are one state and are now written together. Guarded on CoordSys being the active tool, since it is the only card that owns this focus. In the edit path the function runs BEFORE open_tool with the previous tool still active, so the guard is false and the caller's explicit set_xray_focus still wins. Also confirmed while reading, since the header asserts it: set_solid_pick() does NOT touch m_pick_only_body, so the focus really does survive the mesh feed. That claim now has a check behind it rather than a comment. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-bgvk. |
||
|
|
23585382ab |
Mate connector: a roll mark that survives a grazing view, and a quieter warning
Two open findings from the first rig judgement of the connector glyph. F4 — the quadrant collapses to a blob at grazing angles, which is exactly when the roll is hardest to read. Adds a radial tick along +X extending past the disc rim. As the disc flattens to a line the sector loses all its area, but a radial spoke keeps its length and its direction along the one axis that still projects. The alternative on the issue was to billboard the quadrant while the disc stayed in-plane. Rejected, and not on taste: at true grazing the view direction lies IN the connector's plane, so every in-plane direction projects onto the same screen line and the roll is geometrically unrecoverable. Billboarding would not recover it — it would face the camera and read as a definite orientation that is not the frame's. Degrading to a direction that can still be trusted beats drawing a confident lie. The tick is additive, so unlike billboarding it cannot make the non-grazing case worse; it still wants judging on the rig at a true grazing view before F4 is called closed. F5 — roll-undefined was a loud red: the strongest colour in the viewport spent on the least important connector, pulling the eye off the mate being made. It marks "this one could not be derived", not an error. Muted amber says look-here without shouting. No tick is drawn when the roll is undefined — a tick there would assert a direction that does not exist, which is the silent guess the hatched quadrant exists to avoid. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-wgsc. |
||
|
|
e6a14b39c9 |
Rib: the thickness gets its handle, so the whole tool is draggable
Rib's depth already reused the Extrude arrow. Its thickness could not: the arrow points along the plane normal, and thickness is an offset either side of the rib line, IN the plane. Different direction, different handle. Two square handles at mid ± perp·half, plus the slab's actual footprint drawn as a thin closed rectangle — the footprint matters more than the dots, because what a rib thickness means is how wide that slab lands on the body, and until now there was no way to see it before committing. A drag on either handle sets the FULL thickness, twice the perpendicular distance from the line, because the slab is centred on the line and the handle sits at half. Both handles behave identically for the same reason, so they share one colour rather than pretending to be two different actions. A zero-length line has no direction to grow a slab perpendicular to, so the shared rib_frame() helper returns false and render and drag both draw nothing rather than dividing by zero. Non-Line entities clear the gizmo instead of guessing: the kernel is line-only and a gizmo that guesses would be lying about what Confirm will build. Unlike the helix callback this one goes through refresh_preview(), because Rib builds a real solid ghost that has to rebuild. The helix has none and skips it deliberately. Both gizmos coexist and resolve the sketch and entity the same way, so the depth arrow and the thickness handles can never disagree about which line they are on. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-plew. |
||
|
|
b9d6b59f90 |
A feature that destroys a body must say so, not ship a phantom
Driving the control socket: hexagon prism, six vertical fillets, four chamfers on the already-filleted rim, an M8 hole. Afterwards describe_scene reported bodies=3 and error='' — entirely healthy — while body 2's TopoDS_Shape was null. Only mass_properties on that one body revealed anything was wrong. So a feature destroyed a body, recompute() returned true, and the document went on advertising it. Any downstream consumer — slicing, STEP export, a mass properties report — met a null shape with no warning. That is the silent corruption class, which is the one class this project does not tolerate. recompute() now scans the freshly built bodies for a null shape, names the body and the feature that destroyed it, and returns false. Returning false rather than just setting error is the point: it hands the caller its normal rollback path, so the operation that destroyed the body is undone instead of committed. The message says "an unidentified feature" when source_feature is -1. "feature 0" would be a lie, and a message that exists to tell you where to look has to be trusted. TEST IS A POSITIVE CONTRACT, AND THE REASON MATTERS. The reported order was driven headlessly first, as the better test: it does NOT reproduce. The dress-up step throws "fillet radius too large", which is an already-loud already-caught path, so recompute fails honestly and never nulls a body. No public-API sequence found so far reaches the guard's branch without a GUI, and faking a null into `bodies` after the fact would not exercise it — the guard runs on `built`, before the swap. So the test asserts what can be asserted: a box + fillet recomputes true, error is empty, and no body is null. The guard's own branch is defensive and currently unexercised; that is stated here rather than implied by a green suite. Kernel suite: 2217 assertions in 161 test cases, all passing. No existing test relied on a null body surviving a recompute, so hardening this broke nothing. snaporca-5425 (part a). Part b — why the chamfer chain degenerates on an already-filleted rim — is untouched and stays open. |
||
|
|
e8306a6e9a |
Helix: draw the thing, then let the numbers be dragged
grep -i helix over the viewport code returned nothing at all. The tool was four coupled numbers and a Confirm button — you typed radius, pitch, height and taper blind and pressed OK to find out what you had made. So this is not only the charter's L2 failure; the tool had no visible state whatsoever while it was open. Adds a plane-anchored helix gizmo built on the datum-plane gizmo as its template, being the closest existing thing: also plane-anchored, also driven by a card while the sketch tool is inactive, also a render / hit-test / drag triad. It draws the live curve and the axis, and puts a handle on each of the three lengths: radius on the base circle, height at the top of the axis, pitch at the end of the first turn — which is exactly where one pitch of rise lands, so the handle means what it is standing on. Below one full turn the pitch handle moves to the end of the curve rather than floating off a curve that does not exist yet. Taper and handedness stay on the card. One is a shape modifier and the other a flag; L2 governs numbers you can point at. A drag reports the whole (radius, pitch, height) triple rather than one value, because pitch and height are coupled through the turn count and writing one alone would redraw a stale curve. The callback re-feeds the gizmo directly instead of going through refresh_preview(), since Helix takes the produces-no-solid early return and refresh_preview would rewrite the status line on every mouse move. REVIEW CATCH, fixed here: the first cut read taper as a fraction of the radius consumed over the turn count. It is an ANGLE IN DEGREES — helix_spine() builds a Geom_ConicalSurface of half-angle taper and takes the top radius as R+H*tan(taper), growing with the height risen. The wrong reading drew a preview that collapsed to a point for any non-zero taper while the committed feature was perfectly fine. A preview that lies is worse than no preview, which is what this commit replaced. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc. |
||
|
|
a5bb41e340 |
Rib: the depth is the same arrow again
Rib's depth is a distance along the sketch plane normal, so it is the Extrude arrow for the fourth time — anchored at the midpoint of the line the rib is built on, because a rib's line IS its profile. This is half of Rib's L2 failure. The thickness is an in-plane offset either side of that line and no existing gizmo draws that; it needs a handle that does not exist yet, filed as snaporca-plew rather than left implied. One of two numbers draggable is strictly better than neither, and saying which half is missing is the point. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc. |
||
|
|
4ed14eb0be |
SurfaceExtrude and Thicken: drag the distance instead of only typing it
Both tools produce exactly one number — a distance along a known normal — and neither had a handle for it. That is the same shape as the Extrude depth arrow, which was already written, already draggable and already had an editable label on the geometry. So this adds no gizmo: it points the existing one at two more tools. SurfaceExtrude anchors on its sketch's plane, at the profile centroid. Thicken anchors on the picked face, and reuses the face-as-profile recipe from the Extrude path verbatim — including the two things that path learned the hard way: look the face up on its OWNER body rather than the whole-document compound, and carry that body's display Move transform onto both the origin and the normal, or the arrow draws on the bed instead of on the face. The drag callback routes by active tool. `second` stays Extrude's alone: it is the two-sided pair, and the other two have a single distance each. SurfaceOffset is the third tool in this group and is deliberately NOT here. Its target is an arbitrary sheet body, which has no single normal to anchor an arrow on — that is a design decision, not typing, and it stays on the audit. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-i3jc. |
||
|
|
b7397b48bd |
Transform: drag the body, the numbers follow
The Placement > Transform verb opened a card of spin controls — dx/dy/dz, an axis combo, an angle — with nothing on the geometry. The 3-axis drag gizmo the charter asks for already existed and was fully implemented (arrows, rotation rings, click-to-type per axis), reachable only from a small icon button in the tree card header. The prominent verb opened the form; the geometry-first control was hidden behind an icon. That was backwards. Transform now arms that same gizmo on the target body. The card stays as L2's typed half: the drag writes dx/dy/dz, the axis and the angle, and the pivot is seeded from the body's centroid so the parametric feature reproduces exactly what was dragged. Decomposition is exact for the interaction that matters — the gizmo's rings are per-world-axis, so a ring drag is an axial rotation. A pose composed from two rings is not axial and the card can only name one axis, so it reports the dominant one rather than refusing to answer. Three things this had to get right: - The gizmo bakes its drag into the display transform so the body follows the cursor, and the feature performs the same motion parametrically. Committing without reverting first would move the body twice. - tool_confirm() and tool_cancel() both tested moving_body() BEFORE the active tool, so with the gizmo armed Confirm would have dropped the gizmo and never created the feature. Both are now guarded on Tool::None. - close_tool() is the single revert point. Esc, Cancel and switching tools all pass through it, so a Transform that was never committed cannot leave the body displaced. Edit mode is untouched: re-seeding the gizmo from a stored feature is a separate problem, so editing an existing Transform still gets the card alone. Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-qtf4. |
||
|
|
3f62d4d58d |
Bodies: colour that survives selection, hide that toggles twice, Delete that acts
Three defects behind one report ("bodies cannot be moved or hidden/shown or
deleted, colour does not work"). They are unrelated to each other; only the
symptom was shared.
1. The Color tool wrote a per-body override that was correct end to end —
stored on CadBody, carried across recompute (CadDocument.cpp:3381), read
back by DesignCanvas::body_color() — and then overpainted every frame.
m_body_selected is a DOCUMENT-WIDE flag raised whenever a non-Sketch
feature row is selected, which is the resting state after any modelling
operation, and while it was true every body rendered gold. An explicit
colour now outranks the selection tint; unpainted bodies still tint, which
is all the tint was ever for.
2. The eye toggle re-selected the body row through m_tree, using item ids that
belong to m_parts. The row came back unselected, so the second press found
tree_body_selection() == -1 and fell through to the feature-level branch
instead of un-hiding. Hide worked exactly once. The sibling call in
refresh_parts() had it right.
3. The tree card's Delete button answered a selected body row with "select the
FEATURE that created this body" — an instruction the user cannot act on,
because the tree does not say which feature that is. on_delete_body()
already resolves CadBody::source_feature and confirms by name; it was
reachable only from the right-click offer. The button now routes to it.
Reviewed and compiled (libslic3r_gui, RC=0); not exercised — needs a session at
the machine to confirm all three in the viewport. snaporca-zjvg.
|
||
|
|
606026a920 |
Home: axonometric view, fitted
DesignCanvas::set_view() and fit_view() were both written and then never called from anywhere in the tree. The Design viewport has had no way back to a standard view since it existed: no key, no button, nothing but orbiting by hand until the model happens to drift into frame. That is worse than a missing convenience. A camera left pointing along the bed plane renders a scene that looks exactly like a failed renderer — geometry present, nothing visible — and an hour went into blaming the software GL stack before the real cause turned out to be two uncalled functions. Home rather than a letter: every letter A-Z is already a Shift+letter tool shortcut. Home is also the reset-the-view key most users arrive with. The dispatcher needed no change, it keys on the raw wx keycode. set_view() already does select_view + zoom_to_volumes, so this is fit and orient in one call. Doc row added to the View toggles table in docs/design_tab.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b305e8b154 |
tests: compile the five CAD test files that were never wired
tests/libslic3r/CMakeLists.txt added only test_caddocument.cpp under SLIC3R_CAD. The other five shipped in the tree and were never compiled, so 49 TEST_CASE blocks looked like coverage and were not: sketch constraints, sketch editing, sketch import, inference, and the libslvs constraint set. They also still targeted Catch2 v2 — mainline is on v3, where the umbrella header is catch2/catch_all.hpp and Approx lives in the Catch namespace rather than at global scope. Both fixed; nothing else in the files changed. Found by building the tree rather than reading it. Suite goes from 374 to 423 test cases, 54,424 to 54,620 assertions, all passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d584d66003 |
Mate conflicts: name what silently wins, don't call it over-constraint
Two enabled mates driving the same body is not an error today — the later one just wins, and the earlier mate looks ignored with nothing said. A cycle in the mate graph is worse: composition still produces a result, but an arbitrary, order-dependent one. recompute() now fills a mate_conflicts vector of (feature index, reason) before the geometry pass, so it survives a throw further down. It catches a second mate on the same target body, a mate positioning a body against itself, and a cycle, via an iterative three-colour DFS over the body graph. Broken mates are skipped silently — apply_mate() already errors on those. Deliberately non-fatal: recompute() still returns true and error stays empty. Deliberately not "over-constraint" — that word promises DOF analysis from a solver this kernel does not have. Port of snaporca ec4ffeb979. Kernel half of snaporca-bioq. Suite: 2213 assertions / 160 cases green on this fork too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3eec65f2bd |
CoordSys: pick a body first, x-ray the rest
In an assembly the face you want for a mate connector is nearly always behind another body, and a click only ever returns the frontmost hit. Hiding the occluder from the Parts list works but means leaving the tool mid-pick. The CoordSys card now carries a Body chooser. Pick a body and every other one drops to 0.25 alpha AND stops catching clicks, so the wanted face is both visible and reachable in one gesture. "(all)" restores normal picking. Deliberately NOT hit cycling: repeated-click cycling was removed from solid picking as a charter L5/§10 violation (DesignSketchTool.cpp, "NO CYCLE"), and re-introducing it here would make "click a face" a multi-click gesture again. Mechanics: DesignSketchTool::set_pick_only_body() gates body_pickable(), which every pick path already consults; DesignCanvas::set_xray_focus() drives both it and the per-body alpha in reload(). The chooser stays a pick FILTER only -- coordsys_body still comes from the actual pick, so nothing in the kernel moves. Body focus follows the CoordSys card: open_tool() reads it back from the combo rather than clearing outright, because editing a CoordSys feature loads the card (and its body) before open_tool runs. snaporca-bgvk. NOT COMPILED: deps/build lacks OpenVDB so the GUI tree will not configure here; reviewed by diff only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8ff7ba440d |
CadDocument: reindex mate connectors on feature delete and reorder
mate_cs_a/mate_cs_b are feature indices. remove_feature() remapped sketch_ref through the deletion but not the mate connectors, and move_feature() swapped sketch_ref but not the mate connectors. Deleting or reordering any feature ahead of a connector slid both references onto whatever features landed on those slots. Nothing reported it. recompute() only rejects out-of-range and non-CoordSys targets, and a shifted index normally lands on the assembly's other CoordSys — an assembly carries at least two by construction. So the mate resolved against the wrong frames and moved the wrong body, silently. Extracted a remap lambda in remove_feature() and a swap_ref lambda in move_feature(), applied to sketch_ref and both mate connectors. Two tests, both confirmed red before the fix. [mate] tags green here: 395 assertions / 29 cases — the first end-to-end kernel compile of this fork. Ported from snaporca; CadDocument.cpp is byte-identical across forks again. Refs: snaporca-kqih Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0a2faedc32 |
Design: draw the mate connector, so its verse and polarity are visible
A mate connector was visible only to a program. resolve_datum_coordsys had exactly ONE consumer in the whole tree -- McpControl.cpp, the agent socket -- so the frame every mate is built on could not be seen at all, and the two questions a connector has to answer on sight had no answer in the viewport: which way does Z point (the VERSE), and which of the pair is anchored versus about to move (the POLARITY). The glyph is Onshape's proven core plus the part nobody ships. Disc for the XY plane, one gold quadrant for the roll -- the only in-glyph answer to "where is X", which matters because Fastened and Slider lock the clocking -- and a Z arrow drawn on +Z ONLY, never double-headed. Polarity is carried by the head: a filled cone travels, an open collar receives. Onshape, Fusion, Inventor and FreeCAD all draw both ends of a mate identically, which is why "which part moves?" is a standing complaint; nothing here invents new semantics, it just stops hiding them. Polarity is read from the committed Mate features, not only from the open card. A connector some mate drives must read as driven whenever it is on screen, or the glyph tells the truth only while a dialog happens to be open. The card, when open, still wins -- that is the live intent. Judged on the rig rather than in a mock, which changed three decisions: - Three RGB axis arms lose to one Z arrow. Rendered side by side (SNAPORCA_GLYPH=A selects the Onshape-style trio), the three heads are as large as the 22 px disc, they bury the quadrant, and at an oblique angle they pile into a smudge -- and the trio is indistinguishable from the move gizmo and the bed triad, which are already RGB arrow trios in this viewport. - Depth off floats, depth on tears. With GL_DEPTH_TEST off, connectors on faces pointing AWAY from the camera drew their discs over the solid, so the part looked covered in frames that were on its back. Turning depth on fixed that and immediately z-fought: the disc is exactly coplanar with its face and came out a broken dotted arc. Depth ON plus a 0.7*upp lift along Z buys both, and scaling the lift by upp keeps it sub-pixel instead of opening a visible gap on zoom-in. - Foreshortening degenerates an arrow into a dot when the axis points at the camera. It now draws a ring instead of silently vanishing, which is what a naive projection does. Everything is sized in screen pixels via upp = 1/zoom, like every other gizmo here: a connector is a symbol, not a part, so it must not shrink with the model. Research and the empirical findings are written up in DESIGN_MATE_CONNECTORS.md section 8b; rig images in artifacts/shots/g-0*.png, vendor reference glyphs in artifacts/glyphs/. Not fixed here, and recorded rather than papered over: the quadrant collapses to a blob at a grazing angle, which is exactly when the roll is hardest to read (F4); roll-undefined in red makes the least important connector the loudest thing on screen (F5); and a true grazing view, a curved face, and overlap with the move gizmo are still untested. Also surfaced while testing and unrelated to drawing: add_mate accepted a mate between two connectors on the SAME body and duly transformed the body relative to itself -- a concrete instance of the missing validation already filed as G6. Fork parity unchanged: DesignCanvas.cpp 16, DesignPanel.cpp 30, the other four files 0. |
||
|
|
24f4076bb5 |
Design: the Hole and Thread cards say which face they are holding
snaporca-200 asked which of the two models of "the card's face input" is right, because clicking empty canvas now clears the selection (snaporca-od0) and made them visibly disagree: Thicken / Shell / Draft read the LIVE selection and their label reverts to "(pick a solid face)", while Hole / Thread LATCH the face they were opened or picked on and keep it. The complaint was that Hole then drills a face you can no longer see selected. Taken to the rig, that turns out to be the wrong half of the story. With Hole open and its face picked, a click on empty canvas leaves the Ø6.0 ghost and its dimension gizmo drawn on that exact face — the card was never operating in secret, it was showing its target the strongest way a CAD tool can. Meanwhile Draft, whose behaviour was held up as the honest one, threw the pick away and had to be told the face again. So neither model replaces the other. They are different in kind: Thicken / Shell / Draft are operations whose operand IS the selected face, and Hole / Thread are placement tools with their own plane state that a pick merely seeds. The latch is also the kinder of the two now that empty clicks are a deliberate gesture — a stray one costs Thicken a pick and costs Hole nothing. What was genuinely missing is that nothing in those two cards NAMED the latched face, so after such a click the only words on screen were the viewport's "Nothing selected" over a ghost about to drill. Both cards now carry an "On face" row, the way the other three already do: Hole Face 5 | (none — uses Hole plane) Thread Face 1 | Edge 2 | (none — uses Thread plane) Thread names an edge when the cylinder came from a circular rim rather than a cylindrical face, which the code already distinguished internally and never said out loud. Verified on both rigs, every state driven through the GUI: face pick on open and on live pick, survival across a click on empty canvas, and the fallback after choosing XY/XZ/YZ from the plane dropdown. Thread's edge branch was exercised on a revolved tube's rim, its face branch on the same tube's outer wall. Filed while here, surfaced by the new row rather than caused by it — snaporca-uif9: re-editing a stored Hole/Thread from the feature tree restores f.plane into the dropdown but never clears m_hole_on_face, so the re-edit silently reuses the PREVIOUS card's latched face. Now visible by name instead of invisible. Fork parity unchanged: DesignPanel.cpp 30, DesignPanel.hpp 0. |
||
|
|
93395b7888 |
Design: escalate on the entity that was picked, and let the chip follow the window
Both found by Kimi reviewing the previous two commits, both then reproduced here before being touched. snaporca-97z. The re-pick escalation required m_solid_sel, m_sel_body, m_sel_face AND m_sel_edge to all match the previous pick. That looked stricter and was wrong: the edge branch sets only m_sel_edge and m_solid_sel, leaving m_sel_face as whichever face the ray happened to enter through — and a shared edge is entered through a different face depending on which side you view it from. So picking an edge and picking that same edge again from the other side compared equal edges, unequal faces, and refused the escalation the status line had just promised. Now the comparison is made at the level that was picked and nothing else. The edge id is already the stable global one from edge_index_of, so it identifies the edge without help from the face. Reproduced on the rig without needing to orbit, since two clicks 8px apart across an edge enter through different faces: pick -> sel=3 body=0 face=5 edge=3 ray -> body=0 face=0 re-pick -> escalated to whole body 0 pick -> sel=1 body=0 face=-1 edge=-1 Same edge, face 5 then face 0, escalation fires. The old condition could not. The frame-move case. The chip is anchored at an absolute screen position, and until now nothing told it the window had moved — only a resize, a status change or a tab switch re-placed it. Dragging the window by its title bar left it stranded where it was, verified on the rig by moving the frame and watching it stay put. wxEVT_MOVE on the top-level frame, alongside the ICONIZE and ACTIVATE binds from the previous commit. Two related cases are filed rather than bound, because the list of window-geometry events to chase is exactly what snaporca-lcq argues should stop: a layout change that translates the canvas without resizing it, and wxEVT_DPI_CHANGED. Not fixed, deliberately, and recorded on snaporca-97z: clicking the same FACE but landing within the vertex or edge tolerance resolves to a different kind and so does not escalate — that is the "smallest thing under the cursor" rule working as documented; and a vertex re-pick after moving the body compares stale world coordinates. Verified on both rigs. The cross-face edge case was exercised on orca_cad; DesignSketchTool.cpp is byte-identical across the forks, so snaporca inherits it, and its face-level escalation and empty-click clear were re-checked there directly. Fork parity unchanged: DesignSketchTool.cpp 0, DesignCanvas.cpp 16. |
||
|
|
6d1a4078ca |
Design: the status chip goes away with the window, not just with the page
Found by minimising the app on the rig with a face selected: the whole screen goes black and the chip is still drawn on the bare desktop. A wxPopupWindow is override-redirect — the window manager does not own it — so it neither iconises with its frame nor stacks behind other applications. IsShownOnScreen does not catch this either: an iconised frame still counts as shown, which is why the guard added for the tab case sails straight past it. So the frame has to say so itself: ICONIZE and ACTIVATE, both routed through the same show_status_hud the page change already uses. Restoring is safe — a popup cannot take focus, so our own Show() cannot re-trigger either event — and restoring while some other page is up still leaves the chip down, because show_status_hud(true) goes through place_status_hud's IsShownOnScreen guard. Verified on both rigs: chip up, minimise -> screen black and empty, restore -> chip back with its text and the face still selected. Restore while on Prepare -> chip stays down. This is the fourth defect from the same root, so snaporca-lcq now asks the question these binds keep deferring: whether the line should be canvas content, like the view cube and the round view buttons, rather than a window that has to be told about every way a window can stop being visible. Fork parity unchanged: DesignCanvas.cpp 16. |
||
|
|
9c3ce9b45e |
Design: clicking empty space lets go of the selection, and the status line follows its tab
Two things a click on nothing should already have done. snaporca-od0. A click that hit no geometry left the solid selection standing. A rubber band swept over empty space has always cleared it (pick_bodies_in_rectangle), and the two gestures cannot disagree about the same outcome. The visible cost was in the escalation that landed last commit: "click the face, click away, click the face again" arrived as the SECOND click on the same face and took the whole body, when the click away was the user letting go of it. Now the miss clears and says so. This gives up something real, deliberately: Thicken / Shell / Draft hold their input face in the panel's selection, so a stray click on empty canvas with one of those cards open hands that face back. Their handlers already write the "(pick a solid face)" placeholder and rebuild the ghost when the selection empties, so the card SAYS it lost the pick rather than confirming against a face the viewport has stopped highlighting. An orbit drag never reaches this branch — it exits at the 8px budget — so panning the view still does not deselect. snaporca-dlj. The status line is a wxPopupWindow, which is a TOP-LEVEL window: hiding the Design page does not hide it. Select a face, switch to Prepare, and the chip was still there reading "selected (whole body) — right-click for what applies to it" on a tab with no such selection and no such menu. Same cause, second symptom: a status update arriving while the page is hidden anchored against a client size that is not the size the page will have, and parked the chip on the tab bar. So: an IsShownOnScreen guard in place_status_hud, show_status_hud(bool) to take it down and bring it back with its text intact, driven from the page-changed handler. Verified on both rigs, not by reasoning about it: face 5 selected -> click bed -> "Nothing selected", tint gone -> click the same face -> face 5 again, NOT the body -> click it again with no click away -> whole body, so snaporca-gem is intact. Prepare -> chip gone; back to Design -> chip returns. KEYTRACE across the round trip shows shift+S then R still reaching the canvas (ui_mode 0 -> 1, Rectangle armed), which is the focus theft this popup replaced a wxFrame to avoid. Fork parity unchanged: DesignPanel.cpp 30, DesignCanvas.cpp 16, headers and DesignSketchTool.cpp 0. MainFrame.cpp is outside that set and was edited per fork. |
||
|
|
c32aa3f8ba |
Design: clicking the same face twice takes the body, and the status line moves onto the viewport
A click could point at a face, an edge or a vertex, but never at the body those
belong to: offer_selection_kind() can only return BodySolid when all three are
clear, which a viewport click never produces. The rubber band was the only door,
and the status line said "face 5 selected" while the user believed they had taken
the body. A second click on the SAME sub-element now escalates to it (snaporca-gem).
Not the pick cycle that was removed in bc2b741ce9 -- that one was silent and three
deep, so no click had a predictable meaning. Here the status line names the next
click before you make it, and a further click just takes the face under the cursor
again, which needs no teaching. Double-click is untouched: wx sends Down/Up/DClick/Up
and only the first Up carries a pending press, so a fast double-click still zooms to
fit and picks once.
The status line itself moved to the base of the viewport. In the side panel it was
clipped at ~73 characters with no warning and no wrap -- set_status()'s Wrap() never
took effect (snaporca-8cc) -- which silently length-limited every hint in the tab; the
first version of this change lost a clause to it. m_status is kept, hidden, as the
owner of the text and its colour, and the line is drawn in a bottom-left twin of the
readout HUD where there is a whole window's width.
Three defects found driving it on the rig, none of which the build could see:
* the HUD as a wxFrame took the WM's keyboard focus every time it was raised, and
the canvas then received NO key events -- every sketch shortcut silently dead.
Caught with SNAPORCA_KEYTRACE: shift+S logged a line, the following R logged
nothing. It is a wxPopupWindow now, which cannot be focused. SetFocus() on the
canvas does not fix it: focus was on another toplevel.
* zero vertical padding fits the popup tighter than the font's line box and clips
the glyphs; 6 (what the readout uses) reads as a two-line box. 3 is right.
* "has a caller chosen a colour?" compared the label's foreground against its
PARENT's, which differ by default, so every line counted as chosen and the
neutral text came out the panel's dark grey -- invisible on a dark chip. Compare
against the colour the label was created with, captured before any caller writes.
Verified on both rigs against fresh binaries: sketch -> extrude -> click face ->
click again -> whole body tinted, offer opens with the body rows live and Create /
Add material correctly greyed. Keyboard drives the whole sequence.
Filed and NOT fixed here: snaporca-od0 -- a bare-plate click does not deselect the
solid, so "click away, click back" escalates. Pre-existing; clearing there would also
drop the face the Thicken/Shell/Draft cards hold, which needs its own pass.
Refs: snaporca-gem, snaporca-8cc, snaporca-od0
|
||
|
|
7e5994b8cb |
Design: right-click a body row opens the offer, and taking a body always means the same thing
The third door onto the offer, after the viewport right-click and the Menu key. A body ROW is
an unambiguous body, so the offer reports BodySolid and the body verbs act on the row you can
see highlighted — the confirmation a face pick cannot give, since pointing at a face lights the
face and never the body the verb will change. The status line has been promising exactly this
("Body N selected — right-click for what applies to it") since before any handler existed on
that list; the product was advertising a gesture that did nothing.
WHAT THE RIG CAUGHT THAT THE BUILD DID NOT. The first version hung the state normalisation off
wxEVT_TREE_SEL_CHANGED. But SelectItem() on a row that is ALREADY selected fires no selection
event, so a stale vertex from an earlier viewport pick survived — and offer_selection_kind()
tests vertex FIRST, so right-clicking the body row served the VERTEX offer while the row sat
highlighted: Fillet/chamfer/draft greyed, Mirror standing where Repeat belongs, "vertex
selected" still in the status line and the cyan marker still on screen. The happy path (fresh
row, nothing else picked) looked perfect, which is why only the deliberate stale-state sequence
exposed it. Reading the code would not have shown it — SelectItem looks like it selects.
So the normalisation is no longer a selection handler. apply_body_row() is called
UNCONDITIONALLY by both doors, because taking a body from the list means the same state change
however it was asked for. It also clears m_sel_solid_vertex, which the original handler never
did — latent while nothing opened the offer from that list, and immediately fatal once
something did.
Verified on both rigs with the failing sequence itself: pick a vertex, then right-click the
already-selected row. Fillet/chamfer/draft enabled, Repeat back in place, status reads "Body 1
selected", vertex marker gone.
Does NOT touch the feature tree. That needs new selection kinds (offer_selection_kind has no
notion of "a feature is selected") plus verbs the atlas does not contain — Suppress, Rename,
Reorder, Roll back — and is filed separately.
|
||
|
|
6c59898ac0 |
Design: pointing at part of a body is pointing at the body
Tommaso: "i deleted a body using rubber band selection, but this is not intuitive as all the ux revolves around clicking". Correct on both counts, and a correction to what I said last round: the rubber band IS implemented and shipping (pick_bodies_in_rectangle, m_rubber, the drag branch in on_mouse). What is unbound is whole-body picking via CLICK; I read the comment about the click path and wrongly generalised it to the gesture as a whole. The handlers were never the problem either. Move, Mirror, Cut, Mass and Colour all resolve their target through selected_body_default() / m_sel_solid_body, and that is already set when you click a FACE — level >= 1 records the body. They would have worked from a click all along. The only thing keeping them out was the atlas gate: accepts listed body_solid and no face kind, so offer_selection_kind() returning FacePlanar filtered the rows away. This is therefore an atlas-only change, no handler edits. Cut, Split, Mirror, Transform, Mass and Colour now accept face/edge/vertex as well, matching what Delete Body already did. Edges and vertices are included deliberately, not just faces: a click resolves to a vertex, an edge or a face depending on where inside the pixel it lands, so accepting only faces would make Move vanish whenever you clicked near a corner — a flicker that reads as a bug and gets reported as "sometimes it works". NOT widened: Extrude on a face means push/pull THAT face, and Thicken consumes the face you point at. Both have genuine face-specific meaning, so widening them would change what they do rather than where they can be reached from. The rubber band keeps its job — it is still the only way to take a body without also naming one of its faces. It just stops being the only door. Verified on both rigs from a plain face click: Transform > Move opens with Body = Extrude2 (resolved from the face pick), Modify > Edit / Delete Face / Colour / Delete Body, and Reference > Mass. |
||
|
|
b2654ebd8a |
Design: a body knows what made it, so "Delete Body" can exist
Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent faults stacked behind that. FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound (DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but no face kind, so the row was filtered out entirely — while the status line read "Body 1 face 0 selected", which actively teaches the wrong model. SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the FEATURE that created this body". CadBody had no link back to its maker, so the offer was advertising a verb it could not perform — worse than the action:null rows fixed earlier this session, because this one is ENABLED and its refusal reads like user error. CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop, and the rule is just "still unset?". That is sufficient because of an invariant worth stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape (boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the stamp it was born with; a consumed body is erased outright, taking its stamp with it; and the only bodies still at -1 are the ones the current feature just pushed. A feature type added later needs no change here as long as it keeps to that invariant. "Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the body kinds so both can never appear for one selection. Deleting a body removes the feature that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a body vanishing from the viewport is not evidence of which feature went, and this is the one action here that cannot be eyeballed. Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler deletes exactly one body, so a two-body selection would have silently deleted whichever was m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild. Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint -> Undo -> Extrude2 and Body 1 restored. |
||
|
|
34eb4224a1 |
Design: fix a wrong issue ref in the Thicken comment
The previous commit cites snaporca-y7q, which does not exist — I wrote the ID from memory instead of reading it back from the bug I had just filed. The real one is snaporca-kgx, "Offer: Thicken (and peers) open with the picked face discarded". The comment is corrected here; the commit message above it cannot be, so this note is the pointer. |
||
|
|
9d47280a19 |
Design: a card opened from a face must use, and show, that face
snaporca-y7q. Thicken's opener cleared m_sel_solid_face outright. That was right when the
only door was a toolbar button — a button carries no selection, so pressing Thicken had to
clear and ask you to point at something. The offer inverted it: the verb is now invoked ON
a face, and the same line threw away the only thing the user had said. The card opened
reading "(pick a solid face)" over an immediate "thicken: face not found" — you pointed at
the face and were told none could be found.
Keep the pick when the body combo landed on the body it came from (the index is per-body,
and selected_body_default() returns exactly that body when it is valid).
Two neighbours had the mirror-image flaw, both invisible for the same reason — the value
was right and the ghost updated, so only the label lied:
- Thicken had NO live label update at all. Nothing outside the opener ever wrote
m_thicken_face_label, so while the card was open you could pick face after face and it
still read "(pick a solid face)".
- Shell and Draft wrote theirs ONLY from the pick handler, which runs while a card is
already open — so opened from a selection they showed the previous pick, or the
placeholder over a face they were about to use.
So the label is now written once in open_tool(), which every door goes through. The
edit-feature path already restores m_sel_solid_face from the stored feature BEFORE calling
open_tool, so it agrees rather than fights.
Verified on the snaporca rig: face 4 of an extruded plate, offer > Add material > Thicken
now opens "Face: Face 4" with "Preview — 24 triangles" and confirms to a real Body 2. Draft
opened from a face shows "Face 3" and previews the taper. This fork is code-identical here
bar the two permitted DropDown divergences; it still owes a build of its own (snaporca-5pl).
Project keeps its clear: there "(all edges)" is a legitimate default mode rather than a
failure, so changing it would alter behaviour with no reported problem behind it.
|
||
|
|
cfc2555c3a |
Design: a verb's address is data, so the toolbar widget can stop existing
snaporca-7ih's remaining half. Both flyout factories registered their verbs INSIDE the widget-building loop, so the ~40 retired tool buttons had to be constructed and then Hide()n: skipping construction would have deleted 42 offer verbs (26 fly:<family>#<row> + 16 Shift+keys) while their rows still rendered and did nothing when picked. Register first, build second. The addresses are pure data; the widget is one door onto them, not their owner. A family absent from kBarKeep now returns before any wxWindow is made. The keep-list stays a one-line data decision, not a structural one. And close the class of bug for good: the constructor now verifies, once, that every verb the atlas marks wired resolves to a real registration, logging each break and asserting in debug. Rows that render and do nothing have shipped three times (edit_feature and sk_move with action:null, then this) and are invisible from either side alone. Verified on the snaporca rig by walking the offer, not by reading the code — all four at-risk address kinds run with no widget behind them: fly:design_rect#2 drew an OBLIQUE rectangle (the third variant, not the family's first), key:S+E opened Extrude with its 10 mm gizmo, fly:material#4 opened Thicken. Hover hints, icons and nesting intact. This fork is code-identical here bar the two permitted DropDown divergences; it still owes a build of its own (snaporca-5pl). Two hints were wrong and are fixed: Cut said "Split the body with a plane", colliding with the Split verb one row away and pointing at a card for a value the canvas already offers as a draggable arrow; Split never said its plane comes from a picked face. Also, because it blocked the verification and will block the next one: gui-session.sh killed by full path while its own app_pid() matched by basename, so a differently-pathed instance survived, held the single-instance lock, and got reported as a healthy session — a Jul-30 binary nearly passed as this build. It now kills by basename and prints which binary is actually on screen. Traps 6 and 7 documented. |
||
|
|
96816f725c |
Design: every offer verb has a hint, shown on hover — and the status line wraps
Mirror of snaporca 2b3e890165 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared files byte-identical). All 86 verbs now carry a hint: 55 extracted from the C++ tool definitions so the offer and the armed-tool hint cannot drift, 31 written by hand. One wxEVT_MENU_HIGHLIGHT binding shows the hovered verb's hint in the status line. The generator asserts that no wired verb lacks one. Also: all 200 status writes go through set_status(), which wraps instead of clipping at the panel edge; and the empty-document hint is called from on_tab_shown() as well, since after_tree_edit() never runs on a freshly opened tab. Verified on the rig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
3037e55f44 |
Design: hints name the gesture that works, and an empty document says how to start
Mirror of snaporca 033347d062 (parity 30 / 16). Retiring the toolbar made ten hints untrue: each named an action whose door had moved to the offer, or a button no longer on the bar. They now name the gesture. An empty document blanked the status line entirely and now says how to start. Known and not fixed here: m_status does not wrap, so long hints clip; and the 86 offer verbs still have no per-verb hint of their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
811719b7aa |
Design: Construction goes back on the sketch bar — a mode must show its state
Mirror of snaporca b3d4cf85af (parity 30 / 16). Hiding it with the drawing tools was wrong: Construction is a persistent MODE, not a tool — the Bed checkbox, not the Line button. Q and the offer's Construction row kept toggling a checkbox nobody could see, so you could not tell whether the next line would be construction geometry. Scoping unchanged and already correct: m_tb_sketch is shown only in UiMode::Sketch, so it appears exactly while a sketch is open or being edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
86f1f96c50 |
Design: the toolbar is chrome — every tool is reached from the offer
Mirror of snaporca 809aa9df87 (DesignPanel.cpp applied as a patch; parity 30 / 16, shared files byte-identical). fadd() and sadd() now gate what reaches the bar: file operations, Bed, Undo/Redo, Delete selected, Commit to Plate, Confirm/Cancel, plus Place on Face and Section view — the last two because they are chrome_only in the atlas and have no offer row to fall back on. The tool buttons are still built and then hidden, deliberately: their fly: addresses and Shift+key bindings are registered inside the widget-building loops, so not building them would silently drop 42 verbs from the offer while they still rendered. snaporca-7ih covers hoisting the registrations so the construction can go too. Four separators whose groups are now empty were dropped; they rendered as stray rules. Verified on the rig in both modes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
a535b0cb76 |
Design: the offer draws each verb's icon — set the bitmap BEFORE Append, not after
Mirror of snaporca bcab67f8ce (DesignPanel.cpp applied as a patch; parity 30 / 16, shared files byte-identical). tool_atlas.json now names an icon for 80 of 86 verbs, derived from the toolbar's own definitions rather than invented, and every one of the 54 distinct names was checked to exist in resources/images first. The first attempt drew nothing despite a green build: wxGTK builds the GtkMenuItem inside Append() and reads GetBitmap() there, so setting the bitmap on the returned item is a silent no-op. append_offer_item() constructs, sets, then appends — the same order Orca's own append_menu_item() uses. Verified on the rig. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
bc4fb3b680 |
Design: Text and SVG draw INTO the open sketch instead of beside it
Mirror of snaporca 1fb7786d9a (DesignPanel.cpp and DesignCanvas.cpp applied as patches; parity 30 / 16, shared files byte-identical). With a sketch open, Text/SVG outlines become ordinary Line entities via push_closed_lines() instead of a separate Sketch feature carrying rigid imported_regions — so the letters can be constrained, trimmed and extruded like anything drawn by hand. The buttons and offer actions arm Select first when in Sketch mode, since begin_sketch() does not run until a tool is armed. add_imported_regions() calls reset_autoedit(): without it the glyph contours entered the draw-then-edit queue and opened a Length field on the first segment, which freezes the canvas. Caught on the rig, not by reading. No sketch open: unchanged — a new Sketch feature, still dropped on a picked face. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
447c71a0d2 |
Design: Text and SVG join Create — they were excluded on a premise that is not true
Mirror of snaporca 6724ea27c5 (DesignPanel.cpp applied as a patch; parity 30, shared files byte-identical). chrome_only's rule is "acts on the DOCUMENT, not on a selection". Text and SVG both call add_imported_sketch(), which drops the art on a picked solid face via SketchPlane::from_face() — a selection-consuming profile creator, like Sketch. Now sk_text / sk_svg in the sketch half's Create row, where their toolbar buttons already sit. Verified on the rig: Create ends Point, Text, SVG. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
d5c5d5675e |
Design: a body tool acts on the body you picked, not on the first one
Mirror of snaporca e5e223a794 (DesignPanel.cpp applied as a patch; parity 30, hpp byte-identical). Every body combo opened on index 0, so picking a body and pressing Mirror acted on a different solid while the card showed that other body as the target. Nine sites now read the viewport selection; Boolean takes the picked body as target and a different one as tool, since defaulting both to the same body is a no-op. Verified functionally on the rig: picked the 20x20 body, mirrored, and the new body measures 20x20 — not the 80x50 one it would have used before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
273cf067e8 |
Design: record that Shell stays in Remove — decided, not overlooked
Mirror of snaporca e2ef3018cd. Data only, and only the `why` prose in two slots — the generated DesignOffer.hpp is byte-identical, so there is nothing to rebuild. Ratified 2026-08-01: the offer deliberately splits the toolbar's dressup family. Shell hollows a solid so it sits in Remove; Delete Face edits an existing solid so it sits in Modify. Recorded in both slots' `why` so either half explains the split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
f6e6cd83c1 |
Design: the fillet row is named for the tools it actually holds
Mirror of snaporca 352cf1c259 (data only — tool_atlas.json + the regenerated DesignOffer.hpp; both byte-identical across the forks). "Fillet / chamfer / draft" rather than naming shell too: Shell is in Remove and Delete Face in Modify. Only the toolbar's dressup dropdown groups all five. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
0c2b643b0b |
Design: the card says which tool it is, and "Dress-up" stops being a word we use
Mirror of snaporca 3677937964 (DesignPanel.cpp applied as a patch; parity 30, shared files byte-identical). The card header read "Fillet 1" over a chamfer because the offer's Chamfer address opened the tool before setting the type, and open_tool() titles the card from that combo. Choose first, then open. "Dress-up" removed from the offer row (-> "Fillet / chamfer"), the card field (-> "Type", it was a label reading Dress-up whose value said Chamfer) and the toolbar tooltip. Verified on the rig: header "Chamfer 1", field "Type: Chamfer", row "Fillet / chamfer". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
3c998b8a62 |
Design: a tool's options come from the tool, not from a card on the left
Mirror of snaporca 6d5510734b (DesignPanel.cpp applied as a patch; parity re-checked at 30 lines, shared files byte-identical). Polygon's Sides/Circumscribed card is deleted — the choice is made in Create > Polygon, which names the counts and the two fits, because the side count cannot be recovered after drawing. Dress-up, Combine and Pattern were single verbs hiding several behind a combo and now name each one in the offer. Fixes fillet and chamfer both carrying key:S+F, which made the offer's Chamfer open a Fillet. Built green and verified on the rig: the Dress-up card opened from Chamfer reads Chamfer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
d1d61ce997 |
Design: every sketch tool has an address in the offer, not just its family
Mirror of snaporca 0c83f59f13 (DesignPanel.cpp applied as a patch, not copied, so this fork's 30 permitted divergent lines survive; parity re-checked at 30/16 with the shared files byte-identical). The sketch dropdown never registered "fly:<family>#<row>" addresses the way feat_dropdown does for model verbs, so the offer could name a family but only ever arm its first tool — Rectangle always gave a corner rectangle. Adds the registration, 14 atlas verbs (including the entire array family, which was absent, and rotate/scale), an action for the sk_move row that previously did nothing when picked, and a second submenu level so variants nest under their family instead of flattening 19 create tools. Built green and verified on the rig: Oblique rectangle arms oblique, not corner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
fd1bc092d8 |
Design: slot Radius caption, keyboard offer, plane combo removal, mass props, docs
Mirror of snaporca ac85277bac..0e7cb3ec78 (six changes, applied as a patch to DesignPanel.cpp rather than copied, so this fork's 30 permitted divergent lines survive — parity re-checked afterwards: the five shared files are byte-identical, DesignCanvas.cpp and DesignPanel.cpp differ by exactly 16 and 30 lines). - The straight slot's inline field says Radius, which is what it sets. It stores the half-width and passed the typed number through unchanged, so 30 produced a 60 mm slot. - The offer opens from the keyboard (Menu, Shift+F10), anchored on the viewport rather than wherever the pointer happens to be. The card hint names the new route. - The sketch card's Plane combo is gone; the plane comes from the viewport. Also stops build_candidate collapsing a face plane to a base plane while editing. - Mass properties and the dead Edit row are wired into the offer; DesignOffer.hpp is regenerated from tool_atlas.json, verified by re-running the generator and diffing. - docs/rig_build_traps.md + scripts/rig-build.sh, which derives its fork identity from project() so it cannot be pointed at the other fork's image or volume. - docs/design_tab.md refreshed (44 commits stale) + a PR description, with this fork's own merge-base and diff shape rather than snaporca's. Built green in the deps container with the new script and verified on the rig: Menu and Shift+F10 both open the offer at the viewport centre with the pointer parked off-canvas, and the sketch card shows no Plane row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
6e9910303b |
Design: a sketch takes its floating chrome with it when it ends
Confirming a sketch while an inline value field was open left the field behind. The editor is a top-level frame, so it survived the session that owned it, and inline_busy stayed set with it — on_mouse_impl then returned true at its first branch for every later click and the viewport was simply dead. No refusal, no message: exactly the "click the geometry, nothing happens" the pick bugs above it were mistaken for. finish() and cancel() now call close_session_chrome(): dismiss the open field (keep-as-drawn, the same contract the polyline terminators already use), drop the queue of fields behind it, and clear the corner readout — which had the same defect for the same reason, sitting on 336.8° over a committed sketch because nothing redraws the HUD once the tool stops. Verified on the rig on the exact reported sequence: line on XZ, Return to accept the length, Confirm with the Angle field still open. The field goes, the sketch commits, and the next click reaches the pick (pick trace shows down/up consumed) and selects Sketch1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |
||
|
|
1addba6ea0 |
Design: editing a quote UPDATES its dimension instead of appending a rival to it
Type a new length into a sketch quote and the number changed while the geometry
sat still, with the solver dropping to "Conflicting constraints" — a broken
constraint state the user never asked for, arrived at by doing the one thing the
status line invited.
An experiment separated the two candidate causes. Same gesture, one variable:
a line committed WITH a driving length refused the edit and conflicted; a line
committed with none (Esc keeps it as drawn) accepted it and visibly shrank. So
the value was never the problem and the solver was not wrong — it was being
handed two contradicting constraints and correctly declining to choose.
The quote-edit path appended unconditionally:
a.con = int(m_constraints.size());
m_constraints.push_back(constraint_for(a));
Accepting the length at draw time creates Distance(P0,P1) = 64.9. Clicking the
quote later creates a SECOND Distance on the same two points asking for 30. Over
-constrained by construction. A line with no dimension yet only ever gets one
constraint, which is exactly why this looked intermittent rather than total.
upsert_constraint() finds an existing constraint with the same type and operands,
overwrites its value and returns its index; it appends only when there is none.
Operand order is ignored — a Distance from A to B is the same constraint as B to
A, and so is an Angle. Returning the index matters as much as the update: it
keeps the annotation's `con` pointing at the constraint that is actually live, so
the NEXT edit is an update too rather than reverting to appending after one good
round. upsert_dimension() applies the same rule to the visible quote, which had
been stacking labels reading different values on the same pixel, and keeps the
existing label position so a placed quote does not teleport.
set_dimension_value already did the right thing through a.con. The machinery
existed; these two call sites never consulted it.
Verified on :11 on the exact failing case — draw a line, Return to lock the
length, Confirm, double-click to re-open, click the line, type 30 into the quote:
the line shrinks, the quote reads 30.0 mm, one label not two, and the solver
stays at "3 degrees of freedom" with no conflict.
NOT included: record_dimension_constraint() has the same unconditional push_back
in all six of its branches. It belongs to the legacy Constrain mode with
different selection semantics and I could not exercise it, so it is flagged
rather than changed blind.
Refs snaporca-e1p.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
|
||
|
|
a1fdb9f217 |
Design: double-click a sketch stroke to edit it — the gesture belongs on the geometry
Selecting a committed sketch line lit the right tree row and then told the user to go and press Edit in the panel. That is the side-panel dependency this tab exists to remove, and it made "selectable" true while "editable from the geometry" stayed false. on_edit_feature already does the whole job — re-open the entities in the sketch UI with handles and live quotes — and was only ever reachable from a tree row. A double-click on a committed stroke now calls it for that feature. Double-click on empty space still fits the view, so nothing is taken away. The stroke hit test is now one hit_display_sketch() shared by the click and the double-click. Two copies of "what is under the pointer" drift, and a double-click acting on a different entity than the click before it is a miserable thing to chase. It also reports the entity index, which the tracer prints, so a pick that lands on the wrong stroke can be seen rather than inferred. Verified on :11 end to end: draw an open line, commit, double-click it. The tracer prints "double-click -> edit sketch feature 0 (entity 0)", the panel reads "Editing sketch — drag a handle or click a quote to edit", and a click inside the session selects the line with endpoint handles, live quotes and "1 selected — Delete removes them". NOT delivered by this commit, found while verifying it: typing a new value into a length quote is accepted and displayed but the geometry does not move and the solver drops to "Conflicting constraints". Filed separately — it lives in the constraint layer, not in selection, and nothing here touches it. Refs snaporca-e1p. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM |