mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-21 07:52:32 +00:00
d9abadc88f679e48f509381094c66eb5d9cffd75
108
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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 |