Sync cad-mainline with upstream main and carry the value-field + rename work on top

This commit is contained in:
Tommaso Bianchi
2026-09-10 11:01:24 +02:00
47 changed files with 1395 additions and 645 deletions
+59 -33
View File
@@ -83,21 +83,55 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// Onshape-style in-canvas value editor, floating over the GL canvas. The tool hands
// us a screen pixel (device px) + a commit/cancel pair; we convert to logical client
// px and wrap the callbacks so each one re-solves and re-renders the viewport.
m_inline_editor = std::make_unique<SketchInlineEditor>(m_canvas_widget);
m_inline_editor = std::make_unique<SketchInlineEditor>();
// The tool draws it: it owns the frame's ImGui pass and the render scale. Handing it a raw
// pointer rather than the unique_ptr keeps the ownership where it was.
m_sketch_tool.inline_editor = m_inline_editor.get();
// SCHEDULE a paint, do not render one. request_repaint() renders SYNCHRONOUSLY on software
// GL, and this callback runs from inside DesignSketchTool::render() — so using it here asks
// for a render from within a render. The frames stopped after nine, which is what a
// re-entrancy guard giving up looks like. Refresh() posts a paint event instead: the current
// frame finishes, the event loop runs (which is where ImGui's queued characters are consumed),
// and the next frame starts clean.
// MEASUREMENT: does a typed character reach the GL canvas at all? Everything downstream of
// this point is known good (ImGui reports want_text=1 and our InputText active), so if these
// lines do not appear the character never got past the panel's CHAR_HOOK / the focus chain,
// and no amount of work inside the field will help. Skips always: a pure observer.
if (m_canvas_widget != nullptr && std::getenv("ORCA_CAD_UXTRACE")) {
m_canvas_widget->Bind(wxEVT_CHAR, [](wxKeyEvent& e) {
fprintf(stderr, "[UX] canvas_char key=%d\n", e.GetKeyCode());
fflush(stderr);
e.Skip();
});
}
m_inline_editor->request_frame = [this] {
// BOTH halves, and the dirty flag first: GLCanvas3D's paint handler returns without
// rendering when the canvas is not marked dirty, so a bare Refresh() posts an event that
// draws nothing and the frames still stop. request_repaint() does exactly this pair on
// the hardware path; what it must NOT do here is its software path, which renders
// synchronously — and this callback runs from inside render().
if (m_canvas) m_canvas->set_as_dirty();
if (m_canvas_widget) m_canvas_widget->Refresh(false);
};
m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current,
const std::string& title,
std::function<void(double)> commit,
std::function<void()> cancel) {
if (!m_inline_editor) { if (cancel) cancel(); return; }
// The tool hands us canvas device px; convert to logical client px, then to
// absolute screen coords for the floating editor frame.
const double s = m_canvas_widget ? m_canvas_widget->GetContentScaleFactor() : 1.0;
const wxPoint client_pt(int(screen_px.x / s), int(screen_px.y / s));
const wxPoint scr = m_canvas_widget ? m_canvas_widget->ClientToScreen(client_pt) : client_pt;
// The tool hands us canvas device px and the field is now drawn IN the canvas, so this
// is already the coordinate space it wants — no conversion to screen coordinates, and no
// window to place there.
// Freeze the sketch tool while the field is open so a stray click/move on the GL
// canvas can't draw under the floating editor; released on commit or cancel.
// canvas can't draw under the field; released on commit or cancel.
m_sketch_tool.set_inline_busy(true);
m_inline_editor->open(scr, current, title,
// AND PUT THE KEYBOARD ON THE CANVAS. The field is drawn by ImGui, and ImGui is fed from
// GLCanvas3D's own key handler, so a key only reaches it if the canvas is the focused
// widget. That is a focus move WITHIN one window — the toolkit's business, not the window
// manager's, which is the whole point of not being a window any more — but it still has
// to be asked for: after a toolbar click or a tree selection the focus is elsewhere in
// the panel, and the field would sit there taking nothing.
if (m_canvas_widget) m_canvas_widget->SetFocus();
m_inline_editor->open(screen_px, current, title,
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
@@ -1206,7 +1240,7 @@ void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
}
// SetLabel + Wrap + Fit, in that order and always together. Moving the status out of the panel
// removed the clipping of snaporca-8cc but not the underlying problem: the chip is a top-level
// removed the clipping of 8cc but not the underlying problem: the chip is a top-level
// popup that Fit()s to its text, so a long sentence simply grew past the right edge of the canvas
// and hung over the window. Wrapping to the room actually available is what makes the earlier
// promise — "a sentence can be a sentence" — true at every window width, including the charter's
@@ -1240,7 +1274,7 @@ void DesignCanvas::place_status_hud()
const wxPoint bl = m_canvas_widget->ClientToScreen(
wxPoint(kLeftInset, cs.GetHeight() - hs.GetHeight() - 12));
// No Raise() and no focus juggling: a popup neither takes focus nor falls behind. This was
// caught with SNAPORCA_KEYTRACE — shift+S logged a line, the following R logged nothing, and
// caught with ORCA_CAD_KEYTRACE — shift+S logged a line, the following R logged nothing, and
// the only thing between them was the first status update showing this window.
if (!m_status_hud->IsShown()) m_status_hud->Show(); // Show before Move (GTK ignores pre-map Move)
m_status_hud->Move(bl);
@@ -1326,12 +1360,12 @@ void DesignCanvas::delete_selected_sketch_entities()
bool DesignCanvas::inline_busy() const
{
// The TOOL's flag says a value is pending; the FRAME being mapped says a window is on screen
// holding the keyboard. Either one means "a field is up", and only the union of the two is
// safe to route Esc by: the flag alone went false while the frame was still mapped, which is
// the orphan that swallowed every key with nothing able to close it.
// Two sources, still: the TOOL's flag says a value is pending, the editor says a field is
// drawn. They agree now that the field is not a window — the orphan state (logically closed,
// still on screen, still eating keys) cannot be represented when there is nothing to leave
// mapped — but the union costs nothing and is the honest question to ask.
return m_sketch_tool.inline_busy()
|| (m_inline_editor && m_inline_editor->is_mapped());
|| (m_inline_editor && m_inline_editor->is_open());
}
bool DesignCanvas::inline_has_focus() const
@@ -1402,25 +1436,17 @@ void DesignCanvas::open_inline_value(double current, std::function<void(double)>
if (!m_inline_editor || !m_canvas_widget) { if (cancel) cancel(); return; }
// Host-driven value entry (committed-feature Constrain path): the trigger is a toolbar
// button. Anchor the field OVER the picked geometry (same as the draw-then-edit tools) when
// the tool can project it; else fall back to the viewport centre, where the sketch is in
// view. GetScreenRect collapses GetClientSize()+ClientToScreen() into one call; if the GL
// canvas reports degenerate geometry (transiently, right after a re-layout), fall back to the
// always-realised top-level window so the editor never lands in the top-left corner.
wxRect r = m_canvas_widget->GetScreenRect();
if (r.GetWidth() <= 1 || r.GetHeight() <= 1) {
if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget))
r = top->GetScreenRect();
}
wxPoint scr(r.GetLeft() + r.GetWidth() / 2, r.GetTop() + r.GetHeight() / 2);
wxPoint anchor;
if (m_sketch_tool.constrain_value_anchor(anchor)) { // device px in the canvas viewport
const double s = m_canvas_widget->GetContentScaleFactor();
scr = m_canvas_widget->ClientToScreen(wxPoint(int(anchor.x / s), int(anchor.y / s)));
}
// Freeze the canvas so focus-follows-mouse can't steal keyboard focus off the field — the
// same fix the draw-then-edit path uses (cursor focus stays on the field, no pre-click).
// the tool can project it; else fall back to the middle of the canvas, where the sketch is
// in view. Everything here is canvas DEVICE px, the space the field is drawn in.
const wxSize cs = m_canvas_widget->GetClientSize();
const double sf = m_canvas_widget->GetContentScaleFactor();
wxPoint anchor(int(cs.GetWidth() * sf) / 2, int(cs.GetHeight() * sf) / 2);
m_sketch_tool.constrain_value_anchor(anchor); // device px in the canvas viewport
m_sketch_tool.set_inline_busy(true);
m_inline_editor->open(scr, current, "",
m_canvas_widget->SetFocus(); // same reason as the draw-then-edit path: ImGui reads the
// canvas's key events, so the canvas must be the focused widget
m_inline_editor->open(anchor, current, "",
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
+3 -3
View File
@@ -94,7 +94,7 @@ public:
void set_on_segment_drawn(std::function<void(double, double)> cb);
void set_on_cursor_metrics(std::function<void(double, double, bool)> cb);
void set_on_solve_state(std::function<void(int, bool, bool)> cb); // dof, ok, has_constraints
// Live per-step guidance from the armed sketch tool (mode, step, picks). snaporca-1c0c.
// Live per-step guidance from the armed sketch tool (mode, step, picks). 1c0c.
void set_on_sketch_step(std::function<void(DesignSketchTool::Mode, int, int)> cb);
void apply_segment_length(double len); // exact length, then commit & repaint
void keep_segment_as_drawn(); // commit as-drawn & repaint
@@ -219,12 +219,12 @@ public:
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
void set_datum_planes(std::vector<SketchPlane> planes,
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
// Mate connectors, drawn as frames so their verse and polarity are visible (snaporca-wgsc).
// Mate connectors, drawn as frames so their verse and polarity are visible (wgsc).
void set_mate_connectors(std::vector<DesignSketchTool::MateConnectorGlyph> g);
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l);
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
// The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel:
// the panel clips it at ~73 characters with no warning (snaporca-8cc), the viewport's
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
// bottom margin has the whole window width to spare. Empty text hides it.
void set_status_text(const wxString& text, const wxColour& colour);
// Take the status line down / bring it back when the Design page leaves and re-enters view.
+54 -42
View File
@@ -62,7 +62,7 @@
#include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
// English-only pin for the Design tab (see snaporca-design-ux-contract): one lever
// English-only pin for the Design tab (see design-ux-contract): one lever
// de-translates this whole TU so our strings never half-translate against the host's
// localized chrome. Host UI still follows the app locale; only this tab is pinned EN.
// GOTCHA: every _L(...) in this file must take a STRING LITERAL (FromUTF8 wants const char*).
@@ -452,7 +452,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
};
m_keys_sketch['Q'] = [this] {
// With geometry selected, Q converts THAT geometry (snaporca-6zic) — the reading
// With geometry selected, Q converts THAT geometry (6zic) — the reading
// everyone arrives with from other sketchers. With nothing selected it keeps its
// old meaning: arm construction for whatever you draw next.
if (m_viewport && m_viewport->is_sketching() &&
@@ -547,7 +547,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// still BUILT — that is what registers its "fly:<family>#<row>" address and its Shift+key —
// but it is never placed on the bar. Hiding rather than skipping construction is deliberate:
// the addresses are created inside the widget-building loops, so not building would silently
// delete 42 verbs from the offer while they still rendered. snaporca-7ih records the cleanup
// delete 42 verbs from the offer while they still rendered. 7ih records the cleanup
// that lets the construction go away too.
// What stays: the two doc-row imports (consumed by add_doc below) and the view controls,
// which are chrome_only in the atlas and so have no offer row to fall back on.
@@ -577,7 +577,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// outright for a family the bar no longer carries. It used to sit INSIDE the build
// loop, so a retired family still had to be constructed and then Hide()n: skipping it
// would have deleted 42 verbs from the offer while their rows still rendered and did
// nothing when picked. snaporca-7ih.
// nothing when picked. 7ih.
// Keyed on "fly:<family>#<row>" so the generated table can name a variant without the
// item struct growing a field at 26 call sites.
for (size_t i = 0; i < vars.size(); ++i) {
@@ -746,7 +746,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Thicken and were then asked to point at something. Reached from the offer the
// verb is invoked ON a face, so discarding it opened the card reading "(pick a
// solid face)" over an immediate "thicken: face not found" — the user pointed at
// the face and the card said it could not find one. snaporca-kgx.
// the face and the card said it could not find one. kgx.
// The index is per-body, so it only survives if the body combo landed on the body
// it came from; selected_body_default() above returns exactly that when valid.
if (m_thicken_body->GetSelection() != m_sel_solid_body)
@@ -1267,7 +1267,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// The offer reaches each tool by its ratified address; without these the offer could
// name a family but only ever arm its FIRST tool: picking "Rectangle" ran key:R and
// gave you a corner rectangle, with oblique and rounded unreachable. Keyed on the icon
// id (already unique per family) so no call site grows an argument. snaporca-6vs.
// id (already unique per family) so no call site grows an argument. 6vs.
for (size_t i = 0; i < vars.size(); ++i) {
const DesignSketchTool::Mode mode = vars[i].mode;
const wxString hint = vars[i].hint;
@@ -1423,7 +1423,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// the offer's Create > Polygon submenu. They used to sit inline in this row, then in a
// sidebar card; both put the choice somewhere you had to leave the geometry to reach,
// and the count cannot be recovered afterwards (a drawn polygon's inline editor offers
// Side and Angle, never the count). snaporca-e1p.
// Side and Angle, never the count). e1p.
auto arm_polygon = [this, select_tool] {
push_polygon_params();
select_tool(DesignSketchTool::Mode::Polygon,
@@ -1444,7 +1444,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// where you chose the tool — not behind a card you must open to discover it existed.
// Each address opens the tool exactly as its shortcut does, then says which one.
// The members are read at INVOCATION, not capture: the cards are built after this row.
// snaporca-e1p.
// e1p.
auto open_feature = [this](int key) {
auto it = m_keys_feature.find(key);
if (it != m_keys_feature.end() && it->second) it->second();
@@ -1745,7 +1745,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// NO plane row. A sketch takes its plane from what is picked in the VIEWPORT — a planar face
// on a solid, or one of the reference-plane ghosts clicked in 3D — resolved by
// sketch_plane_from_selection(). A three-row XY/XZ/YZ combo could not express either of those
// targets, so it displayed a value that was at best redundant and at worst false. snaporca-e1p.
// targets, so it displayed a value that was at best redundant and at worst false. e1p.
m_width = make_spin(m_cards, 20);
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL);
@@ -1808,7 +1808,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// tool — the offer's Create > Polygon submenu names the common side counts and the two
// fits, and arming from there sets both. A spin field on the left could not be reached
// without leaving the geometry, and the count is unrecoverable afterwards: the inline
// editor a drawn polygon opens offers Side and Angle, never the count. snaporca-e1p.
// editor a drawn polygon opens offers Side and Angle, never the count. e1p.
}
// --- Extrude dialog (consumes the selected sketch) ---
@@ -3027,7 +3027,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// on a solid, or one of the reference-plane ghosts — because that is where the user is
// looking and pointing. A combo duplicated that decision somewhere the geometry could not
// see it, and once a face could be picked it went further and displayed a stale row that
// contradicted the real target. snaporca-e1p.
// contradicted the real target. e1p.
// Kept as a member, not a local: the card has to be able to STOP saying this. It asked
// for a plane even when one had just been picked, directly contradicting the status line
// two inches below it, which by then read "Sketching on XZ".
@@ -3138,7 +3138,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// "slow double-click renames" the old comment promised does not survive wxGTK, which fires
// ITEM_ACTIVATED first), none of the seven header icons renames, and F2 is a function key
// nothing announces. A user who wants to name a sketch tries the row, and now the row
// answers. snaporca-rename.
// answers. rename.
m_tree->Bind(wxEVT_TREE_ITEM_RIGHT_CLICK, [this](wxTreeEvent& e) {
m_tree->SelectItem(e.GetItem()); // right-click targets what it points at
const int sel = tree_selection();
@@ -3622,7 +3622,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Selection no longer writes the status line: on_sketch_step owns it, says the same thing
// for Select mode and — unlike this callback, which also fired while an edit-op mirrored its
// picks into the selection — never claims "N selected, Delete removes them" in the middle of
// a Mirror gesture, where Delete does nothing of the sort. snaporca-1c0c.
// a Mirror gesture, where Delete does nothing of the sort. 1c0c.
// Onshape flow: clicking inside a closed-loop face commits the sketch and opens
// the Extrude dialog (with a ghost preview) targeting that sketch.
@@ -3667,7 +3667,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// exists to remove. Only a stroke hit carries an entity (an interior click is a region,
// not a line), so a click inside a loop deliberately leaves the field alone rather than
// resetting it to something arbitrary. The sketch picker follows the same pick, so
// pointing at a line in a different sketch retargets both together. snaporca-3648.
// pointing at a line in a different sketch retargets both together. 3648.
if (m_active == Tool::Rib && entity >= 0) {
if (m_rib_sketch != nullptr)
for (unsigned i = 0; i < m_rib_sketch->GetCount(); ++i)
@@ -3741,7 +3741,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_sel_solid_edge = (level == 3) ? edge : -1;
m_sel_solid_vertex = (level == 4);
// Keep the hit face even at whole-body level: the cycle's first click means "this body",
// but the user pointed AT a face and a sketch should be able to use it. snaporca-3a2.
// but the user pointed AT a face and a sketch should be able to use it. 3a2.
m_pick_face_body = (level >= 1) ? body : -1;
m_pick_face = (level >= 1) ? face : -1;
// Last pick wins: selecting a solid drops any stale committed-sketch loop selection.
@@ -3762,7 +3762,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
if (m_active == Tool::Dressup) { sync_dressup_target(); update_fillet_gizmo(); refresh_preview(); }
// Boolean card open: the VIEWPORT is how you choose the two operands. Until now they
// could only come from two combos — the one control the charter names for this tool
// (e1p item 4), and the same pair snaporca-7xx caught silently resolving every row to
// (e1p item 4), and the same pair 7xx caught silently resolving every row to
// index 0. The highlight already flowed card -> viewport; this closes the loop the
// other way. First pick is the target (kept), second is the tool (consumed); the
// combos mirror both, so the typed half of L2 still works and still round-trips.
@@ -3787,7 +3787,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
}
// Mirror card open: the body you point at is the body that gets mirrored. Same one-way
// flow Boolean had (snaporca-310o) and the same fix — the combo stays as the typed half.
// flow Boolean had (310o) and the same fix — the combo stays as the typed half.
// Only one operand here, so there is no slot to alternate and no swap to do.
if (m_active == Tool::Mirror && m_sel_solid_body >= 0 && m_mirror_body != nullptr &&
m_sel_solid_body < int(m_mirror_body->GetCount())) {
@@ -3914,12 +3914,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_status->SetForegroundColour(wxNullColour);
const int nb = int(m_doc.bodies.size());
const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString();
// Each sub-element line ends by naming the NEXT click (snaporca-gem). Escalation to the
// Each sub-element line ends by naming the NEXT click (gem). Escalation to the
// whole body is a gesture nothing on screen would otherwise reveal, and the status line
// is the only surface that can teach it at the moment it applies. It REPLACES the old
// per-level verb hints ("right-click to push/pull it", "Fillet/Chamfer to dress it")
// rather than joining them: the line is clipped at the panel edge past ~55 characters
// (set_status's Wrap() does not take effect — snaporca-8cc), and those verbs are shown
// (set_status's Wrap() does not take effect — 8cc), and those verbs are shown
// with their icons in the offer anyway, while this gesture is shown nowhere else.
// Both clauses fit now that the line is drawn over the viewport instead of squeezed
// into the panel. Say "what applies to it", never "verbs" — that is this codebase's
@@ -4173,18 +4173,25 @@ DesignPanel::DesignPanel(wxWindow* parent)
// select_tool() is what the sketch keys call — so the first letter after entering
// sketch mode fell through to the feature map, matched nothing (feature keys are
// Shift+letter), and did nothing. The mouse worked only because the toolbar flyout
// reaches select_tool() directly. That is why all 17 keys read as dead. snaporca-0ud.
// reaches select_tool() directly. That is why all 17 keys read as dead. 0ud.
const bool sketch_mode = (m_ui_mode == UiMode::Sketch);
// Never steal editing keys from a focused text field or an open in-canvas value field —
// Delete/Ctrl+Z there must edit the text, not the model.
const bool in_text = (dynamic_cast<wxTextCtrl*>(wxWindow::FindFocus()) != nullptr)
|| (m_viewport && m_viewport->inline_busy());
if (getenv("SNAPORCA_KEYTRACE")) {
if (getenv("ORCA_CAD_KEYTRACE")) {
wxWindow* fw = wxWindow::FindFocus();
fprintf(stderr, "[KEYTRACE] key=%d ui_mode=%d is_sketching=%d in_text=%d inline_busy=%d focus=%s\n",
key, int(m_ui_mode), (m_viewport && m_viewport->is_sketching()) ? 1 : 0, in_text ? 1 : 0,
(m_viewport && m_viewport->inline_busy()) ? 1 : 0,
fw ? (const char*) fw->GetClassInfo()->GetClassName() : "(none)");
// wxString, not a cast: GetClassName() returns const wxChar* — wchar_t* in
// this build — and casting THAT to const char* and printing it with %s emits
// the first byte and stops at the padding NUL. Every focus= field this tracer
// has ever printed was a single letter: "wxGLCanvas" came out as "w", and so
// did "wxWindow". An instrument that silently truncates its most important
// field is worse than no instrument, and this one was trusted for a whole
// day's diagnosis.
fw ? wxString(fw->GetClassInfo()->GetClassName()).utf8_str().data() : "(none)");
fflush(stderr);
}
@@ -4205,6 +4212,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_viewport->inline_commit();
return;
}
// NO forwarding here any more. The field is drawn INSIDE the GL canvas now, so it
// is fed the way every other ImGui widget in this app is fed: GLCanvas3D::on_char ->
// ImGuiWrapper::update_key_data -> io.AddInputCharacter. Re-adding a panel-side
// forwarder would also mask whether that path works, which is exactly what is being
// measured.
// Esc is NOT special-cased here any more: escape() routes it, and the open field is
// exactly what CadLevel::Transient means, so it closes the field and stops there.
}
@@ -4244,7 +4256,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Delete — the selected sketch entities (or the last drawn one if none is selected), or the
// selected feature in Feature mode. Focus-independent, same reason as undo above.
// WXK_BACK too: on a keyboard whose Del is a chord (every laptop this runs on), Del is
// the one destructive key nobody can reach, and Backspace is what users press. snaporca-oql1.
// the one destructive key nobody can reach, and Backspace is what users press. oql1.
if (!in_text && (key == WXK_DELETE || (key == WXK_BACK && sketching))) {
if (sketching) { m_viewport->delete_selected_or_last_sketch_entity(); return; }
if (m_ui_mode == UiMode::Feature && m_active == Tool::None
@@ -4439,7 +4451,7 @@ void DesignPanel::set_ui_mode(UiMode m)
if (m != UiMode::Sketch) m_sketch_on.clear(); // no stale "on the picked face" on the next hint
// The DoF readout describes a SKETCH's constraint state, so it means nothing back in Feature
// mode — where it nonetheless stayed on screen after every Confirm, Cancel and Escape
// (snaporca-752). Cleared here rather than at those three exits because this is the one place
// (752). Cleared here rather than at those three exits because this is the one place
// all of them pass through, and a fourth exit added later would otherwise reintroduce it.
// Constrain mode keeps it: that is where the number is the whole point.
if (m == UiMode::Feature && m_dof_status != nullptr) {
@@ -4679,7 +4691,7 @@ static void run_off_ui_thread(wxWindow* parent, const wxString& message, const s
//
// This used to be written in exactly ONE place — on_commit(), as a side effect of Commit to
// Plate — so a user who modelled for an hour and pressed Ctrl+S saved a project containing no
// feature history at all, and the app reported success (snaporca-vjk5). The 3MF exporter was
// feature history at all, and the app reported success (vjk5). The 3MF exporter was
// never at fault: nothing had handed it a recipe.
//
// Every save path (Ctrl+S, Save As, autosave, crash recovery) reads model.cad_recipe, so
@@ -5068,7 +5080,7 @@ void DesignPanel::on_add_extrude()
} else if (extrude_uses_loop()) {
// Extrude just the selected loop (its entity subset), leaving the source sketch's
// other loops intact and still selectable.
if (::getenv("SNAPORCA_PICK_TRACE"))
if (::getenv("ORCA_CAD_PICK_TRACE"))
std::fprintf(stderr, "[pick] on_add_extrude: feat=%d reg=%d ents=%zu\n",
m_extrude_sketch_ref, m_sel_sketch_region,
m_viewport->selected_loop_entities().size());
@@ -5140,11 +5152,11 @@ void DesignPanel::on_add_dressup()
// Hole and Thread LATCH the geometry they were opened or picked on; Thicken / Shell / Draft read
// the live selection instead. Both models are right for what they are — a placement tool with its
// own plane state, versus an operation whose operand IS the selected face — and the latch is the
// kinder of the two now that a click on empty canvas clears the selection (snaporca-od0): a stray
// kinder of the two now that a click on empty canvas clears the selection (od0): a stray
// click costs a Thicken pick, and costs a Hole nothing. What was missing is that nothing in the
// Hole/Thread card NAMED the latched face, so after such a click the only words on screen were
// the viewport's "Nothing selected" — over a ghost still drawn on the face Confirm would drill.
// That reads as a contradiction and was filed as one (snaporca-200). The card now says what it
// That reads as a contradiction and was filed as one (200). The card now says what it
// holds, the way the other three cards already do. Pass -1 for "none, using the plane dropdown".
void DesignPanel::set_hole_target_label(int face)
{
@@ -6013,7 +6025,7 @@ SketchPlane DesignPanel::plane_from_choice(int row) const
// clicking one of the ghost planes in 3D (on_datum_base_picked) rather than by opening the combo.
// Before this, a picked face was ignored and the only way onto it was to build a Coincident datum
// plane first and then find it in a dropdown — three steps and a junk feature in the tree for the
// most common gesture in solid modelling. snaporca-3a2.
// most common gesture in solid modelling. 3a2.
SketchPlane DesignPanel::sketch_plane_from_selection(wxString& what) const
{
SketchPlane p;
@@ -6068,7 +6080,7 @@ bool DesignPanel::sketch_map_applies() const
// actually REACHED, so the menu describes what is highlighted — a header that names a face while
// the whole body is lit would be lying, and this menu's whole value is that it tells the truth
// about the selection. (Sketching on the face you merely clicked is unaffected: that path is
// sketch_plane_from_selection, which deliberately uses m_pick_face. snaporca-3a2.)
// sketch_plane_from_selection, which deliberately uses m_pick_face. 3a2.)
int DesignPanel::offer_selection_kind() const
{
if (sketch_map_applies()) {
@@ -6191,7 +6203,7 @@ void DesignPanel::set_status(const wxString& text)
// sets the colour on it just before calling here, so this stays the one place that knows
// both. What the user reads is drawn along the BASE OF THE VIEWPORT: in the panel the line
// was clipped at ~73 characters with no warning and no wrap (Wrap() never took effect —
// snaporca-8cc), which silently length-limited every hint in the tab. The viewport's bottom
// 8cc), which silently length-limited every hint in the tab. The viewport's bottom
// margin has the whole window width, so a sentence can be a sentence.
if (m_viewport != nullptr) {
// wxNullColour means "no opinion", and the dark default text colour is nearly invisible
@@ -6206,7 +6218,7 @@ void DesignPanel::set_status(const wxString& text)
}
// The sentence for the step the armed sketch tool is on (snaporca-1c0c). One table, so a tool's
// The sentence for the step the armed sketch tool is on (1c0c). One table, so a tool's
// gesture is described in one place and the description cannot drift from the code that reads the
// clicks: the step counts here are the ones DesignSketchTool::render previews and on_mouse
// consumes. `step` = anchors already placed (edit-ops: 0 none, 1 first pick down, 2 ready to
@@ -6402,7 +6414,7 @@ wxMenuItem* DesignPanel::append_offer_item(wxMenu* menu, int id, const wxString&
// honest source for that is the loop that builds the rows.
static void offer_trace(const char* fmt, ...)
{
static const bool on = std::getenv("SNAPORCA_KEYTRACE") != nullptr;
static const bool on = std::getenv("ORCA_CAD_KEYTRACE") != nullptr;
if (!on) return;
va_list ap;
va_start(ap, fmt);
@@ -6418,13 +6430,13 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
const int kind = offer_selection_kind();
const uint32_t bit = offer_bit(OfferSel(kind));
// Which verb MAP applies is a question about the MODE, not about whether a session is
// running — the same distinction the keyboard already had to learn (snaporca-0ud). Gated on
// running — the same distinction the keyboard already had to learn (0ud). Gated on
// is_sketching() the offer opened on entering a sketch showing the FEATURE rows, every one
// of them refusing the sketch selection, so it read as a menu of nine dead entries.
const bool sketching = sketch_map_applies();
// The offer ladder reads THIS, not the pixels: the trace is emitted from the same loop that
// builds the menu, so it cannot drift from what the user is shown. Gated on the existing
// SNAPORCA_KEYTRACE so a rig run needs one env var, not two. snaporca-<offer ladder>.
// ORCA_CAD_KEYTRACE so a rig run needs one env var, not two. <offer ladder>.
offer_trace("open kind=%d sketching=%d bodies=%d", kind, sketching ? 1 : 0,
int(m_doc.bodies.size()));
@@ -6539,7 +6551,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
}
}
// --- Mate palette section (snaporca-lukg part B) ---
// --- Mate palette section (lukg part B) ---
// Fed by CadDocument::mate_options() so the offer can never disagree with the kernel about
// which assembly mates a connector pair admits. Shown only when the document holds at least
// two ENABLED CoordSys features: below that the whole section would be one permanently dead
@@ -7008,7 +7020,7 @@ wxString DesignPanel::idle_hint() const
: _L("No solid yet — select a sketch and right-click it to Extrude.");
}
// The Design tab is no longer the visible page (snaporca-dlj). The status line is a popup floating
// The Design tab is no longer the visible page (dlj). The status line is a popup floating
// over the GL canvas, so it does NOT go away when this page does — it stayed up over Prepare and
// over the home screen, still reading like a live Design selection ("selected (whole body) —
// right-click for what applies to it") on a tab that has no such selection and no such menu.
@@ -7111,7 +7123,7 @@ void DesignPanel::refresh_tree()
// recompute() returns FALSE for a document that has no solid ("no solid-producing features",
// CadDocument.cpp) — which is precisely a document the user has only drawn sketches in. So
// drawing a profile, pressing Confirm and saving wrote a 3MF with no orca_cad.bin in it
// at all, and the app reported success: the whole design was gone on reopen (snaporca-mtav).
// at all, and the app reported success: the whole design was gone on reopen (mtav).
// The three sites that say "a lone sketch yields an empty body; that is expected" call
// m_doc.recompute() directly and so never reached the sync either. One hook here covers all
// of them, including the live sketch tool's own commit path.
@@ -9080,7 +9092,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
m_hole_through->SetValue(f.hole_through);
m_hole_x->SetValue(f.hole_x);
m_hole_y->SetValue(f.hole_y);
// Re-latch the on-face state FROM THE STORED FEATURE (snaporca-uif9). m_hole_on_face is
// Re-latch the on-face state FROM THE STORED FEATURE (uif9). m_hole_on_face is
// only ever cleared by the Hole flyout and by the plane combo, so after any on-face hole
// it stays true — and a re-edit then drilled on whatever face was latched last, which may
// be a different face, a different body, or a body since rebuilt. f is the only source
@@ -9111,7 +9123,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
m_thread_x->SetValue(f.thread_x);
m_thread_y->SetValue(f.thread_y);
if (m_thread_std) m_thread_std->SetSelection(0); // Custom: spins reflect the stored feature
// Same latch, same failure, same fix as Hole above (snaporca-uif9).
// Same latch, same failure, same fix as Hole above (uif9).
m_thread_on_face = !is_base_plane(f.plane, m_doc.modeling_origin);
m_thread_face_plane = f.plane;
m_thread_face_body = m_thread_on_face ? f.target_body : -1;
@@ -9690,7 +9702,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const
// The plane is STRUCTURAL, like Extrude's profile source. While EDITING it is preserved
// from the seeded original — the card carries no plane control and the old combo silently
// collapsed a face plane to a base plane through the modeling origin. While ADDING it
// comes from what is picked in the viewport. snaporca-e1p.
// comes from what is picked in the viewport. e1p.
if (!editing) { wxString where; f.plane = sketch_plane_from_selection(where); }
f.width = m_width->GetValue();
f.height = m_height->GetValue();
@@ -10020,7 +10032,7 @@ void DesignPanel::update_fillet_gizmo()
}
// Push the active Hole card's plane + position + diameter/depth to the viewport gizmo.
// Grey the FEATURE buttons whose tool cannot run yet, and say why in the tooltip (snaporca-o9j).
// Grey the FEATURE buttons whose tool cannot run yet, and say why in the tooltip (o9j).
// Tommaso reported the array controls as MISSING; they were not, but Pattern with no body
// accepted the click, opened nothing, and wrote its refusal somewhere other than where the click
// happened — from the user's seat that is indistinguishable from a dead button. A control that
+8 -8
View File
@@ -57,7 +57,7 @@ public:
void clear_document(); // New Project / Open Project: drop the document with the project
// Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature
// op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result.
// Push the document's recipe into the Model so ANY save path persists it (snaporca-vjk5).
// Push the document's recipe into the Model so ANY save path persists it (vjk5).
void sync_recipe_to_model();
bool recompute_guarded(const wxString& message);
@@ -173,7 +173,7 @@ private:
// Which body a tool should act on when it opens: the one picked in the VIEWPORT, else
// the first. Selection comes first and the tool consumes it — every body combo used to
// default to index 0, so picking body 3 and opening Mirror silently mirrored body 1.
// Clamped to the list, so it is safe to hand straight to SetSelection. snaporca-e1p.
// Clamped to the list, so it is safe to hand straight to SetSelection. e1p.
int selected_body_default() const;
void populate_body_choices(int as_of_feature = -1);
// Fill `c` with the bodies as they existed just before `as_of_feature` and select
@@ -279,7 +279,7 @@ private:
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
SketchPlane thread_plane() const;
// Name the geometry the card has LATCHED, so it never has to be inferred from the viewport.
// Pass -1 for "none, falling back to the plane dropdown". See snaporca-200.
// Pass -1 for "none, falling back to the plane dropdown". See 200.
void set_hole_target_label(int face);
void set_thread_target_label(int face, int edge);
CadFeature build_candidate(Tool t) const;
@@ -489,13 +489,13 @@ private:
// Polygon's two parameters are chosen FROM THE TOOL, in the offer's Polygon submenu, not
// from a card on the left: the side count cannot be edited after drawing (the inline editor
// offers Side and Angle only), so it has to be settled at the moment the tool is armed —
// which is exactly where the offer already is. snaporca-e1p.
// which is exactly where the offer already is. e1p.
int m_poly_sides{6}; // 3..64; the submenu names the common ones
bool m_poly_circumscribed{false};
// Which reference plane a sketch falls back to when no face is picked: 0/1/2 = XY/XZ/YZ,
// >=3 indexes resolve_datum_planes(). Set by CLICKING a ghost plane in the viewport — there is
// deliberately no dropdown for it. snaporca-e1p.
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
// from "nobody has chosen anything yet". This does.
@@ -703,7 +703,7 @@ private:
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
// it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering
// that a second click refines the selection, so keep it instead of throwing it away. snaporca-3a2.
// that a second click refines the selection, so keep it instead of throwing it away. 3a2.
int m_pick_face_body{-1};
int m_pick_face{-1};
// What the live sketch was actually opened on ("the picked face", "XY", a datum's name), so the
@@ -767,7 +767,7 @@ private:
double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0};
// Says which face the latch above is holding. Thicken/Shell/Draft show theirs because their
// face IS the live selection; this one has to be shown precisely BECAUSE it is not, and the
// status line goes on saying "Nothing selected" while the ghost keeps drilling. snaporca-200.
// status line goes on saying "Nothing selected" while the ghost keeps drilling. 200.
wxStaticText* m_hole_target_label{nullptr};
ComboBox* m_thread_plane{nullptr};
@@ -885,7 +885,7 @@ private:
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
// readout (the live length/angle while a segment is being dragged) can be appended to it
// instead of replacing it — the guidance used to vanish on the first mouse move after a
// click, which is precisely when it is needed. snaporca-1c0c.
// click, which is precisely when it is needed. 1c0c.
wxString m_sketch_step;
// mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does
// not include the tool's, and the .cpp (which does) casts it back.
+59 -38
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <imgui/imgui.h>
@@ -307,7 +308,7 @@ void DesignSketchTool::set_tool(Mode mode)
void DesignSketchTool::cancel()
{
close_session_chrome(); // same orphaned-field freeze as finish() — see snaporca-yce
close_session_chrome(); // same orphaned-field freeze as finish() — see yce
m_active = false;
m_step_mode_last = -1;
m_points.clear();
@@ -452,7 +453,7 @@ void DesignSketchTool::delete_selected()
// now-deleted entity and freeze the flow" — it was simply never called from here. Measured:
// delete a rectangle whose Width/Height were still queued, draw a circle, type its radius —
// the field opens, the digits go in, and the radius does not move, because the field belongs
// to a rectangle that no longer exists. snaporca-ua9g.
// to a rectangle that no longer exists. ua9g.
reset_autoedit();
// And re-solve, so the sketch's reported degrees of freedom describe the sketch that is
@@ -462,7 +463,7 @@ void DesignSketchTool::delete_selected()
if (on_selection_changed) on_selection_changed(0);
}
// Convert the selection to/from construction geometry (snaporca-6zic). The Construction
// Convert the selection to/from construction geometry (6zic). The Construction
// checkbox only ever set the mode for what you draw NEXT, so a line drawn as real geometry
// could never become a guide, nor a guide become real. Whole Feature groups flip together:
// a rectangle is four Line entities and converting three of them is never what was meant.
@@ -1383,10 +1384,22 @@ std::string DesignSketchTool::dimtype_title(DimType k) const {
// Draw-then-edit dispatcher: mirror the Select-mode quote-click logic, but target the
// freshly-drawn selection's PRIMARY value and use the tentative (clean-cancel) path for
// scalar quotes. Runs after render_live_quotes, so the live-quote state is populated.
// Why a draw-then-edit chain did not start. Four early returns can swallow it, and from outside
// they are indistinguishable: the shape appears, no field opens, and nothing says which guard
// fired. check-gui-click-edit.py reports that as "a value field opened (nothing did)" for every
// tool at once, which reads like a total product failure and is not necessarily one.
static void trace_autoedit(const char* why, size_t n)
{
if (!std::getenv("ORCA_CAD_UXTRACE")) return;
fprintf(stderr, "[UX] autoedit %s steps=%zu\n", why, n);
fflush(stderr);
}
void DesignSketchTool::open_primary_autoedit()
{
if (!on_inline_edit || m_awaiting_length) return; // no host, or a field is already open
if (!m_active) return; // session ended before the deferred tick
if (!on_inline_edit) { trace_autoedit("skip: no on_inline_edit host", 0); return; }
if (m_awaiting_length) { trace_autoedit("skip: a field is already open", 0); return; }
if (!m_active) { trace_autoedit("skip: session ended before the deferred tick", 0); return; }
// Build ONE ordered list of edit steps covering EVERY characteristic dimension of the
// freshly-drawn shape — scalar quotes (constraint-based) AND geometric editors — so every
@@ -1494,6 +1507,8 @@ void DesignSketchTool::open_primary_autoedit()
[this, fi](double v){ set_rect_angle(fi, v); }, span(fi), "Angle" });
}
trace_autoedit(m_autoedit_dims.empty() ? "built NO steps (no live quote matched)" : "opening",
m_autoedit_dims.size());
if (!m_autoedit_dims.empty()) {
m_autoedit_dim_idx = 0;
open_next_autoedit_dim();
@@ -2393,7 +2408,7 @@ bool DesignSketchTool::try_add_constraints(const std::vector<SketchEntityConstra
m_constraints.resize(mark); // roll back the conflicting batch
// No re-solve to "restore": a failed solve no longer touches the geometry
// (SketchSolver.cpp only writes back on success), so m_entities still holds the
// prior solved state exactly. snaporca-pl5.
// prior solved state exactly. pl5.
return false;
}
@@ -2501,11 +2516,11 @@ void DesignSketchTool::infer_auto_constraints(int base, double ang_tol_rad, doub
// never costs the others. Every rule only pins a relation that is ALREADY true, so
// nothing the user drew is moved by this.
// A SCRIPTED ADD IS NOT A DRAWN GESTURE — the rule this function already states at its
// bulk call site, which passes zero tolerances for exactly that reason (snaporca-8xg1).
// bulk call site, which passes zero tolerances for exactly that reason (8xg1).
// Relational inference must obey it too, and for a second reason beyond tolerance:
// EqualRadius couples entities that are geometrically far apart, so on a real drawing it
// merges independent connected components into one huge system and defeats the
// component partitioning that makes large sketches solvable at all (snaporca-yww4).
// component partitioning that makes large sketches solvable at all (yww4).
// Measured 2026-08-31 on the corpus rung: geometry stayed correct (32/32 sheets clean)
// but seven of the largest sheets hit main-thread timeout — MPD681 among them, the very
// sheet named in the comment at the bulk call site. Exact-equality would not save it
@@ -2636,7 +2651,7 @@ bool DesignSketchTool::add_imported_regions(
// Art is not "just drawn", so it must NOT enter the draw-then-edit queue. Without this the
// glyph contours are treated as fresh entities and a Length field opens on the first of
// them — on a word, that is one value editor per segment, and an open field freezes the
// canvas (snaporca-yce). reset_autoedit() marks every entity as already seen.
// canvas (yce). reset_autoedit() marks every entity as already seen.
reset_autoedit();
// The new lines carry no constraints, so the solver has nothing to move; resolve anyway so
// the degrees-of-freedom readout counts them instead of going stale.
@@ -3107,7 +3122,7 @@ void DesignSketchTool::hit_display_sketch(const DisplaySketch& d, const Vec2d& p
{
const std::vector<RegionLoop> loops = region_loops(d.entities);
// What did the sketch decompose into, and what is under the click? This is the trace that
// settled snaporca-txp8 — it prints the loop table with each loop's hole count, so
// settled txp8 — it prints the loop table with each loop's hole count, so
// "containment is wrong" and "the click landed elsewhere" stop being indistinguishable.
// Guarded rather than merely silent: hit_display_sketch runs on every pick, and the message
// costs a string build and a heap allocation per loop even when nothing consumes it.
@@ -3145,7 +3160,7 @@ void DesignSketchTool::hit_display_sketch(const DisplaySketch& d, const Vec2d& p
if (h >= 0 && h < int(loops.size()) && point_in_poly(p, loops[h].poly)) { in_hole = true; break; }
if (!in_hole) { face_feat = d.feature; face_reg = r; }
}
// edge_ent is printed because it is now DELIVERED (snaporca-3648) — a tool can ask for the
// edge_ent is printed because it is now DELIVERED (3648) — a tool can ask for the
// line you pointed at, not just its loop, and "which entity did that click resolve to" is
// otherwise unanswerable from outside.
dp_pick_trace("region hit -> feat=%d reg=%d (edge_feat=%d edge_reg=%d edge_ent=%d)",
@@ -3249,11 +3264,11 @@ void DesignSketchTool::select_body(int body)
// Pick tracing. Selection failures on a real desktop have repeatedly turned out to be an
// event that never arrived rather than a ray that missed, and the two look identical from
// the UI. Set SNAPORCA_PICK_TRACE=1 and the whole press->release->ray path narrates itself
// the UI. Set ORCA_CAD_PICK_TRACE=1 and the whole press->release->ray path narrates itself
// on stderr. Off by default: no cost, no noise.
static bool dp_pick_trace_on()
{
static const bool on = ::getenv("SNAPORCA_PICK_TRACE") != nullptr;
static const bool on = ::getenv("ORCA_CAD_PICK_TRACE") != nullptr;
return on;
}
@@ -3279,7 +3294,7 @@ static void dp_pick_trace(const char* fmt, ...)
//
// ponytail: crossing over a triangle sample set. A rectangle small enough to sit entirely
// inside one flat triangle selects nothing — drag a bigger one, or click. Real multi-body
// selection (and the homogeneous-set rule that goes with it) is snaporca-9xw.
// selection (and the homogeneous-set rule that goes with it) is 9xw.
void DesignSketchTool::pick_bodies_in_rectangle()
{
if (m_solid_mesh == nullptr || m_solid_tri_body == nullptr || m_solid_bodies == nullptr)
@@ -3471,7 +3486,7 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent
m_sel_vertex_pt = p.vertex_pt;
m_solid_sel = p.kind;
// CLICK AGAIN ON THE SAME THING -> THE WHOLE BODY (snaporca-gem). Pointing at a face and
// CLICK AGAIN ON THE SAME THING -> THE WHOLE BODY (gem). Pointing at a face and
// pointing at its body are different intents, and until now only the rubber band could
// express the second one — so the status line said "face 0 selected" while the user
// believed they had taken the body, and every body verb had to opt into the face kinds to
@@ -3808,23 +3823,23 @@ void DesignSketchTool::clear_extrude_gizmo()
// cone travels, an open collar receives. No surveyed CAD system encodes this at all; both ends of
// their mates are drawn identically, which is why "which part moves?" is a standing complaint.
//
// SNAPORCA_GLYPH=A|B selects the treatment while this is being judged on the rig:
// ORCA_CAD_GLYPH=A|B selects the treatment while this is being judged on the rig:
// A three short axis arms, no head differentiation (the Onshape baseline)
// B one-sided Z arrow, filled vs open head (the proposal) -- default
void DesignSketchTool::render_mate_connectors()
{
if (m_mate_connectors.empty()) return;
static const bool style_A = [] {
const char* s = ::getenv("SNAPORCA_GLYPH");
const char* s = ::getenv("ORCA_CAD_GLYPH");
return s && (*s == 'A' || *s == 'a');
}();
// The face treatment, on by default. Read every frame rather than latched in a static, so
// toggling the preference takes effect on the next repaint instead of at the next launch —
// it is a look, and a look you cannot A/B without restarting will not get compared.
// SNAPORCA_GLYPH=D forces the disc regardless, which is how the rig drives the other branch.
// ORCA_CAD_GLYPH=D forces the disc regardless, which is how the rig drives the other branch.
const bool face_style = !style_A
&& wxGetApp().app_config->get_bool("design_connector_face_glyph")
&& [] { const char* s = ::getenv("SNAPORCA_GLYPH");
&& [] { const char* s = ::getenv("ORCA_CAD_GLYPH");
return !(s && (*s == 'D' || *s == 'd')); }();
const Camera& cam = wxGetApp().plater()->get_camera();
@@ -4006,7 +4021,7 @@ void DesignSketchTool::render_mate_connectors()
}
// ---------------------------------------------------------------------------------------------
// THE FACE TREATMENT of the mate connector (snaporca-x0kd). The disc + roll quadrant answers
// THE FACE TREATMENT of the mate connector (x0kd). The disc + roll quadrant answers
// "where is X" with a shape that has to be learned; a face does not. Face orientation is
// hardwired perception -- a toddler reads a face's roll and verse with no instruction at all --
// and that is the whole reason this exists. Default ON, switchable in Preferences for users who
@@ -4038,7 +4053,7 @@ static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW
static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.
{-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786},
};
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z).
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z).
static const Vec3d kBearMarks[] = {
{-0.1997, +0.1760, +0.0590},
{+0.1947, +0.1760, +0.0590},
@@ -6421,7 +6436,7 @@ DesignSketchTool::region_loops(const std::vector<SketchEntity>& ents) const
// NESTING. A loop drawn inside another one is that one's HOLE. Without this a sketch is
// just N disjoint filled polygons, so "the plate with the hole" is not expressible and the
// multi-loop kernel path (snaporca-88v) is unreachable from the viewport — which is exactly
// multi-loop kernel path (88v) is unreachable from the viewport — which is exactly
// what Tommaso hit: a rectangle with a circle inside extruded to a plain box, because only
// the rectangle loop could be picked and only its entities were passed on.
//
@@ -6435,7 +6450,7 @@ DesignSketchTool::region_loops(const std::vector<SketchEntity>& ents) const
// polygon being tested answers by rounding, so the same drawing can be read either way.
// Measured on the StudyCadCam corpus: the engine and an independent containment check
// disagreed on 6 of 39 sheets, and every disagreement was a probe point sitting on the other
// loop's boundary. snaporca-5hvl.
// loop's boundary. 5hvl.
auto poly_area = [](const std::vector<Vec2d>& q) {
double a2 = 0.0;
for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++)
@@ -6535,7 +6550,7 @@ int DesignSketchTool::region_at(const Vec2d& p) const
// ---- rendering --------------------------------------------------------------
// Chop a polyline into dashes (snaporca-imlq). Construction geometry is dashed in every CAD;
// Chop a polyline into dashes (imlq). Construction geometry is dashed in every CAD;
// this one painted it solid grey, which against the under-constrained orange reads as "another
// line", not as "reference only". The dash and gap arrive in WORLD units — the caller scales them
// by units-per-pixel, so the dash keeps its size on screen at any zoom instead of turning into a
@@ -7971,7 +7986,7 @@ void DesignSketchTool::confirm_op()
// The sources as they stand BEFORE any of this op's constraints exist. Two jobs: every
// copy is reflected from the untouched original (so a batch that moves the sketch cannot
// feed a later copy moved geometry), and the invariant at the bottom has something to
// compare against. snaporca-mirror-slot.
// compare against. mirror-slot.
const std::vector<SketchEntity> before = m_entities;
const size_t cmark = m_constraints.size();
std::vector<std::pair<int, SketchEntity>> fresh; // copy index -> its pristine reflection
@@ -8018,7 +8033,7 @@ void DesignSketchTool::confirm_op()
// postcondition on the geometry, and if a source moved it keeps the copies — which are
// exactly what the preview showed — and drops the whole constraint web that moved them.
// Restoring the sources needs no re-solve: the pre-batch state was itself solved, and a
// failed solve does not write back (snaporca-pl5).
// failed solve does not write back (pl5).
// BOTH HALVES. Watching only the sources caught the slot (whose web dragged everything)
// and missed the rounded rectangle, where the solver held the sources still and put the
// COPIES somewhere else: an arc has five degrees of freedom and Symmetric on centre plus
@@ -8434,7 +8449,7 @@ const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const
return nullptr;
}
// Which step of the armed gesture is live, reported only when it moves (snaporca-1c0c). Called
// Which step of the armed gesture is live, reported only when it moves (1c0c). Called
// from render(), which is the one place EVERY state change passes through — a per-call-site
// notification would have to be added to each of the thirty-odd tool branches and would be
// forgotten by the next one. Cheap: three ints compared per frame.
@@ -8465,6 +8480,11 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
m_dim_label_seq = 0;
m_render_scale = canvas.get_scale();
emit_step_hint(); // before the early returns: an armed tool on an empty sketch still guides
// The value field, BEFORE every early return below. It can be up in Constrain mode on a
// committed feature and on an empty sketch, and a field that is not drawn is a field that is
// not there — there is no window to fall back to any more.
if (inline_editor != nullptr)
inline_editor->render(*wxGetApp().imgui(), m_render_scale);
(void)canvas;
if (!has_display()) {
if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD
@@ -8733,7 +8753,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
// Mirror's is the axis, Fillet/Chamfer's is the first of the two lines — and until now
// every pick painted the same white, so the picture could not answer "what did I select
// as what". Violet, not cyan: cyan means SELECTED here and nothing else may wear it.
// snaporca-vd6v.
// vd6v.
const bool op_ref = is_edit_op_mode() && int(i) == m_op_a;
ColorRGBA col;
if (editing_this) col = editing;
@@ -8813,7 +8833,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
if (!sel_handles.empty()) draw_vertices(m_highlight_model, sel_handles, sel_col);
// Midpoint of every segment, drawn smaller and cooler than the endpoint handles
// (snaporca-te8v). Without it the Midpoint snap is invisible: it exists in the
// (te8v). Without it the Midpoint snap is invisible: it exists in the
// inference engine but the user has nothing to aim at. Construction lines get one
// too — you constrain to them as readily as to real geometry.
std::vector<Vec2d> mids;
@@ -8857,6 +8877,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
// next render_live_quotes(), so the deferred open still sees this frame's values.
if (m_autoedit_pending) {
m_autoedit_pending = false;
trace_autoedit("pending -> deferring open", 0);
wxGetApp().CallAfter([this] { open_primary_autoedit(); });
}
if (is_edit_op_mode())
@@ -9334,7 +9355,7 @@ int DesignSketchTool::add_entities_scripted(const std::vector<SketchEntity>& ent
// the 39 corpus drawings the loops that came back wrong were all TINY (1.4 to 13 mm^2), out
// by up to 7e-4 relative, because a 0.005 degree tilt on a 0.3 mm chord is inside 1e-4.
// With zero, only a segment that is EXACTLY axis-aligned is constrained, and constraining
// something already true cannot move it. snaporca-8xg1.
// something already true cannot move it. 8xg1.
// The weld window closes too. Two endpoints a micron apart are not the same point when a
// caller typed both of them: on MPD681, 20 of 363 scripted segments were dragged onto a
// common point up to 0.0021 mm away, because welding is TRANSITIVE and three vertices near
@@ -9349,7 +9370,7 @@ int DesignSketchTool::add_entities_scripted(const std::vector<SketchEntity>& ent
// while m_awaiting_length) and swallows every letter (in_text includes inline_busy()). The
// symptom was that the first key and click after sketch_add did nothing until one Escape had
// dismissed the field. Resyncing the baseline here leaves an ALREADY open field alone; it
// only stops this add from being read as something the user just drew. snaporca-j7gc.
// only stops this add from being read as something the user just drew. j7gc.
m_autoedit_seen = int(m_entities.size());
return base;
}
@@ -9561,7 +9582,7 @@ bool DesignSketchTool::select_at_screen(GLCanvas3D& canvas, int sx, int sy)
// counts only m_selection, so right-clicking a sketch point produced the EMPTY
// vocabulary and every SkPoint row in the atlas was unreachable from the menu. Other
// entities keep the handle pick: a line's endpoint is a drag target, not a thing with a
// vocabulary of its own. snaporca-lnri.
// vocabulary of its own. lnri.
if (ei >= 0 && ei < int(m_entities.size())
&& m_entities[ei].type == SketchEntity::Type::Point) {
if (std::find(m_selection.begin(), m_selection.end(), ei) != m_selection.end())
@@ -9643,8 +9664,8 @@ std::vector<int> DesignSketchTool::connected_loop(int seed) const
// suppresses the menu whenever it is set, so right-click became a no-op that also hid the one door
// to half the vocabulary (47 of 86 verbs have no shortcut). Measured on the rig: with Line armed,
// two right-clicks in a row produced no menu and no tool change; only Escape freed it.
// Same rule as snaporca-xmh6, which said it for the selection: clearing nothing is not a gesture
// terminator. snaporca-ghcz.
// Same rule as xmh6, which said it for the selection: clearing nothing is not a gesture
// terminator. ghcz.
bool DesignSketchTool::right_abandon()
{
if (m_points.empty())
@@ -9979,7 +10000,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
// consumed from here on. Left-drag no longer orbits in this canvas — DesignCanvas puts
// orbit on middle-drag and pan on right-drag, the CAD convention — so nothing downstream
// is being starved of a gesture it used to own.
// HOVER PRE-HIGHLIGHT (snaporca-9xw part 3): say what a click would take, before it is
// HOVER PRE-HIGHLIGHT (9xw part 3): say what a click would take, before it is
// taken. Plain motion only — no button down, no band running — because during a drag the
// pointer is doing something else and a promise about clicking would be a lie. Returns
// false so the event still reaches the camera; this only asks for a repaint, it does not
@@ -10069,7 +10090,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
return true;
}
m_display_pick = -1; m_display_pick_region = -1; // clicked bare plate -> drop highlight
// ...and the SOLID selection goes with it (snaporca-od0). A click that hits nothing has to
// ...and the SOLID selection goes with it (od0). A click that hits nothing has to
// mean what a rubber band that sweeps nothing already means — pick_bodies_in_rectangle
// clears on an empty sweep, and the two gestures cannot disagree about the same outcome.
// Until now the face survived a click on bare plate, so "click away, then click the face
@@ -10717,7 +10738,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
return true;
}
if (evt.RightDown() && m_points.empty())
return false; // no chain to end — snaporca-ghcz, let the offer open
return false; // no chain to end — ghcz, let the offer open
if (evt.RightDown()) {
// END the chain — do NOT close it. This used to call push_closed_lines() for three
// or more points, i.e. it drew a final segment from the last point back to the
@@ -11096,7 +11117,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
return true;
}
if (evt.RightDown() && m_points.empty())
return false; // no poles down — snaporca-ghcz, let the offer open
return false; // no poles down — ghcz, let the offer open
if (evt.LeftDClick() || evt.RightDown()) {
if (m_points.size() >= 2) {
const int base = int(m_entities.size());
+11 -8
View File
@@ -64,7 +64,7 @@ public:
Constrain };
// Which tool is armed, and how many anchors it has down. Read-only, for the offer ladder:
// "the menu armed the verb I chose" is otherwise unassertable, and a menu walk that lands one
// row off arms a NEIGHBOURING tool and then grades whatever that drew. snaporca-ekt9.
// row off arms a NEIGHBOURING tool and then grades whatever that drew. ekt9.
Mode mode() const { return m_mode; }
int pending_points() const { return int(m_points.size()); }
void emit_step_hint(); // fires on_step_changed when the step actually moved
@@ -133,13 +133,16 @@ public:
bool has_entities() const { return !m_entities.empty(); }
bool on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas);
// Right-click on a draw tool: true when an in-progress anchor was abandoned, false when
// there was nothing to abandon — and false is what lets the offer menu open. snaporca-ghcz.
// there was nothing to abandon — and false is what lets the offer menu open. ghcz.
bool right_abandon();
// True if the LAST right-press was consumed as a gesture terminator (end a polyline chain,
// abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to
// decide whether that right-click was the user's, in which case it opens the offer.
bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; }
void render(GLCanvas3D& canvas);
// The in-canvas value field, drawn by render() before any early return. Owned by
// DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp.
class SketchInlineEditor* inline_editor{nullptr};
// Persistent committed sketches to draw even when no session is active (e.g. an
// un-consumed sketch left visible after its extrude is removed). Each carries its
@@ -210,7 +213,7 @@ public:
// feature index + the clicked closed-region index within it (-1 = no specific loop).
// entity = the sketch entity index under the cursor when the click landed on a loop
// STROKE, else -1 for an interior/region hit. Carried because a tool can legitimately
// want the LINE you pointed at, not just the loop it belongs to (Rib, snaporca-3648).
// want the LINE you pointed at, not just the loop it belongs to (Rib, 3648).
std::function<void(int feature, int region, int entity)> on_display_sketch_selected;
// Double-click on a committed sketch stroke: open THAT feature for editing. Selecting a line
// and then hunting for an Edit button in a panel is the dependency this tab exists to remove.
@@ -339,7 +342,7 @@ public:
// Mate connectors. Until now a connector was visible only to a program — resolve_datum_coordsys
// had exactly one consumer, the MCP socket — so the frame a mate is built on could not be seen
// at all. The glyph has to answer two questions on sight (snaporca-wgsc): which way does Z point
// at all. The glyph has to answer two questions on sight (wgsc): which way does Z point
// (the VERSE), and which of the pair is anchored versus driven (the POLARITY). Nothing in any
// surveyed CAD system encodes the second one.
struct MateConnectorGlyph {
@@ -437,7 +440,7 @@ public:
// Live readout while drawing a Line/Polyline segment (anchor->cursor metrics).
std::function<void(double length, double angle_deg, bool locked)> on_cursor_metrics;
// Live step guidance (snaporca-1c0c). The armed tool reports WHICH STEP of its gesture the
// Live step guidance (1c0c). The armed tool reports WHICH STEP of its gesture the
// user is on, every time that changes, so the status line can name the next click instead of
// repeating the one-shot sentence written when the tool was armed. step = anchors/picks
// already down (Mirror: 0 = no axis, 1 = axis down, 2 = ready to apply); picks = size of the
@@ -676,7 +679,7 @@ private:
// weld_tol: how far apart two endpoints may be and still be called Coincident.
// Both default to GESTURE slack. A scripted add passes zero for both: the caller has
// already said exactly what it means, and every non-zero window is a window in which the
// inference rewrites it. snaporca-8xg1.
// inference rewrites it. 8xg1.
void infer_auto_constraints(int base, double ang_tol_rad = 3.0 * M_PI / 180.0,
double weld_tol = 1e-3);
@@ -936,7 +939,7 @@ private:
// A selectable sketch region: its own boundary, plus the loops nested INSIDE it, which
// are its holes. Modelling holes is what makes "the plate with the hole in it" a thing the
// user can point at — without it a sketch is N disjoint filled polygons and the only
// selectable things are the rectangle alone or the circle alone (snaporca-txp8).
// selectable things are the rectangle alone or the circle alone (txp8).
struct RegionLoop {
std::vector<Vec2d> poly;
std::vector<int> ents;
@@ -1162,7 +1165,7 @@ private:
Vec3d vertex_pt{Vec3d::Zero()};
};
bool resolve_solid_pick(GLCanvas3D& canvas, int mx, int my, SolidPick& out) const;
// HOVER PRE-HIGHLIGHT (snaporca-9xw part 3). Vertex-beats-edge-beats-face is a rule the user
// HOVER PRE-HIGHLIGHT (9xw part 3). Vertex-beats-edge-beats-face is a rule the user
// cannot see until after they commit to a click; showing the outcome under the pointer is
// what makes the precedence learnable at all, and is the charter's L5 (one click, one visible
// change) read honestly — the change has to be predictable before the click, not only after.
+3 -3
View File
@@ -113,7 +113,7 @@ json describe_tools()
// Hand-written descriptor. The bridge turns this into MCP tool schemas; later
// slices grow this list (ideally from the kernel directly).
return json{
{"app", "SnapOrca CAD"},
{"app", "Orca CAD"},
{"protocol", "jsonrpc-2.0"},
{"slice", 5},
// Read this before using any face or edge id.
@@ -2201,9 +2201,9 @@ void server_thread(std::string sock_path)
void start_mcp_control_if_enabled()
{
const char* env = std::getenv("SNAPORCA_MCP");
const char* env = std::getenv("ORCA_CAD_MCP");
if (!env || !*env) return;
std::string path = (std::strcmp(env, "1") == 0) ? "/tmp/snaporca-mcp.sock" : env;
std::string path = (std::strcmp(env, "1") == 0) ? "/tmp/orca-cad-mcp.sock" : env;
static bool started = false;
if (started) return;
started = true;
+4 -4
View File
@@ -3,9 +3,9 @@
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
// tab. Off unless the env var SNAPORCA_MCP is set:
// SNAPORCA_MCP=1 -> socket at /tmp/snaporca-mcp.sock
// SNAPORCA_MCP=/path/to.sock -> socket at that path
// tab. Off unless the env var ORCA_CAD_MCP is set:
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
// All CAD work is marshalled onto the wx main thread and runs through the SAME
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
// describe_tools, describe_scene, extrude.
@@ -15,7 +15,7 @@
namespace Slic3r { namespace GUI {
// Start the server thread iff SNAPORCA_MCP is set. Safe to call once after the
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
void start_mcp_control_if_enabled();
+149 -314
View File
@@ -1,368 +1,203 @@
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/I18N.hpp" // _L for the refusal messages shown in the title line
#include <wx/display.h>
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "libslic3r/Color.hpp"
#include <wx/frame.h>
#include <wx/textctrl.h>
#include <wx/stattext.h>
#include <wx/sizer.h>
#include <wx/window.h>
#include <wx/toplevel.h>
#include <wx/gdicmn.h>
#include <imgui/imgui.h>
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#ifdef __WXGTK__
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#endif
#include <cstring>
#include <string>
namespace Slic3r {
namespace GUI {
namespace {
// Locale-safe value <-> text (wx sets LC_NUMERIC to the user locale, so snprintf may
// emit a comma; parsing accepts either separator). Mirrors DesignPanel's en_*.
wxString en_format(double v, int digits = 2)
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
std::string fmt_value(double v, int digits = 2)
{
char fmt[16];
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
char buf[64];
std::snprintf(buf, sizeof(buf), fmt, v);
for (char* c = buf; *c; ++c) if (*c == ',') *c = '.';
return wxString::FromUTF8(buf);
}
bool en_parse(const wxString& text, double& out)
{
wxString t(text);
t.Replace(wxT(","), wxT("."));
return t.ToCDouble(&out);
for (char* c = buf; *c; ++c)
if (*c == ',') *c = '.';
return std::string(buf);
}
// Present the toplevel with a real X11 server timestamp: wxFrame::Raise() maps to
// gtk_window_present() with gtk_get_current_event_time(), which inside a CallAfter is
// GDK_CURRENT_TIME (0) and is ignored by mutter's focus-stealing prevention. A server
// timestamp lets the compositor grant focus to the re-mapped window.
#ifdef __WXGTK__
void present_toplevel(wxFrame* frame)
bool parse_value(const char* text, double& out)
{
#ifdef GDK_WINDOWING_X11
if (frame) {
GtkWidget* widget = static_cast<GtkWidget*>(frame->GetHandle());
if (widget && GTK_IS_WIDGET(widget)) {
GdkWindow* gdkwin = gtk_widget_get_window(widget);
if (gdkwin) {
gtk_window_present_with_time(GTK_WINDOW(widget),
gdk_x11_get_server_time(gdkwin));
return;
}
}
}
#endif
if (frame) frame->Raise();
if (text == nullptr) return false;
std::string t(text);
for (char& c : t)
if (c == ',') c = '.';
// strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a
// number. "12mm" must be refused, not silently read as 12.
const char* b = t.c_str();
char* end = nullptr;
const double v = std::strtod(b, &end);
if (end == b) return false;
while (*end == ' ' || *end == '\t') ++end;
if (*end != '\0') return false;
out = v;
return true;
}
#else
void present_toplevel(wxFrame* frame)
{
if (frame) frame->Raise();
}
#endif
// Between two queued dimensions (a rectangle's Width then Height) the frame is either kept
// MAPPED and merely re-titled, or unmapped and mapped again. That is a per-toolkit choice, not
// a preference:
// GTK/mutter keep it mapped. Focus-stealing prevention refuses keyboard focus to a window
// that was just re-mapped, so hiding between the two fields left the second one
// visible but dead (snaporca-p8uw).
// elsewhere map it afresh. This is what shipped before that workaround, which was applied
// with no platform guard — and it is the only difference between the first queued
// field (works everywhere) and the second (macOS wedges the whole app, PR #15238).
// A workaround for one window manager must not become a contract for all of them.
constexpr bool keep_mapped_between_fields =
#ifdef __WXGTK__
true;
#else
false;
#endif
void trace_inline_focus(wxFrame* frame, const std::string& title)
// One machine-readable line per event of the click-edit contract, for the UX check that runs
// after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as
// ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its
// format is a contract the script parses.
//
// The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the
// prefill, so a field that is on screen but not editable commits its prefill and the two lines
// disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number
// the user actually gets can.
void ux_trace(const char* event, const std::string& title, const std::string& detail)
{
if (!std::getenv("SNAPORCA_KEYTRACE")) return;
#ifdef __WXGTK__
GtkWindow* win = nullptr;
if (frame) {
GtkWidget* widget = static_cast<GtkWidget*>(frame->GetHandle());
if (widget && GTK_IS_WIDGET(widget)) win = GTK_WINDOW(widget);
}
fprintf(stderr, "[INLINE_FOCUS] title=%s active=%d toplevel_focus=%d shown=%d\n",
title.c_str(),
win ? (int) gtk_window_is_active(win) : -1,
win ? (int) gtk_window_has_toplevel_focus(win) : -1,
frame ? (int) frame->IsShown() : -1);
#else
fprintf(stderr, "[INLINE_FOCUS] title=%s shown=%d\n",
title.c_str(), frame ? (int) frame->IsShown() : -1);
#endif
fflush(stderr);
if (!std::getenv("ORCA_CAD_UXTRACE")) return;
std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str());
std::fflush(stderr);
}
} // namespace
// The title line doubles as the error line, so both colours live here rather than as a
// literal at the one place that used to set it.
// Keep the frame fully on-screen: an anchor that maps off the display makes GTK drop the
// window at a default corner (top-left) instead of the requested point. Clamp to the display
// the anchor is ON, not the primary one — wxGetClientDisplayRect() only ever describes the
// primary monitor, so on a multi-head desktop this shoved the field onto a different screen
// than the app. It then sat invisible while m_awaiting_length made the sketch tool eat every
// mouse event, which read as the viewport freezing after a sketch, with only Enter able to
// release it. Shared with the error re-fit below, which can widen the frame after placement.
static wxPoint clamp_to_display(wxPoint pos, const wxSize& sz, const wxPoint& anchor, wxWindow* w)
{
int disp = wxDisplay::GetFromPoint(anchor);
if (disp == wxNOT_FOUND) disp = wxDisplay::GetFromWindow(w);
const wxRect area = (disp != wxNOT_FOUND) ? wxDisplay(unsigned(disp)).GetClientArea()
: wxGetClientDisplayRect();
pos.x = std::max(area.GetLeft(), std::min(pos.x, area.GetRight() - sz.GetWidth()));
pos.y = std::max(area.GetTop(), std::min(pos.y, area.GetBottom() - sz.GetHeight()));
return pos;
}
static const wxColour kTitleFg (160, 162, 168);
static const wxColour kTitleErr(232, 106, 106);
SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas)
{
m_parent = parent_canvas; // where the keyboard goes back to when this field lets go
wxWindow* top = parent_canvas ? wxGetTopLevelParent(parent_canvas) : nullptr;
// Borderless floating frame: a top-level window so the WM composites it above the
// GL canvas (a child widget would be hidden by the GL surface). Floats on its
// parent and stays on top so it tracks the main window.
// NB: no wxFRAME_FLOAT_ON_PARENT — that maps to a GTK _UTILITY_ window-type hint, which
// many WMs (incl. the xrdp/x11vnc session on :10) refuse to give keyboard focus, so the
// field opened un-focusable and needed a click before typing. Plain stay-on-top frame is
// WM-focusable; we present + SetFocus it explicitly in open().
m_frame = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxSTAY_ON_TOP);
m_ctrl = new wxTextCtrl(m_frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(82, -1),
wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_SIMPLE);
m_frame->SetBackgroundColour(wxColour(40, 42, 46));
m_title = new wxStaticText(m_frame, wxID_ANY, wxEmptyString);
m_title->SetForegroundColour(kTitleFg);
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(m_title, 0, wxLEFT | wxRIGHT | wxTOP, 3);
sizer->Add(m_ctrl, 1, wxEXPAND | wxALL, 2);
m_frame->SetSizerAndFit(sizer);
m_frame->Hide();
m_ctrl->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { do_commit(); });
// The complaint goes away the moment the user starts answering it — an error that
// outlives the input it was about is just noise on the next attempt.
m_ctrl->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { clear_invalid(); e.Skip(); });
m_ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) {
// Esc on an ORPHAN (mapped, m_open already false) must still take the field off the
// screen. do_cancel() returns early there, and while the frame holds the X input focus
// this handler is the ONLY code the keyboard can still reach — so if it refuses, nothing
// else gets a turn and the application looks frozen.
if (e.GetKeyCode() == WXK_ESCAPE) { if (m_open) do_cancel(); else dismiss(); }
// Tab commits, exactly like Enter — the caller's on_commit is what walks to the next
// dimension. Left to wx's default handling it navigated within this one-control popup,
// i.e. back to the same field with the text re-selected: typing 60, Tab, 40 looked like
// two dimensions entered and silently kept only the 40. Losing typed input with no
// visible difference from a committed field is the part that made this worth a key case.
else if (e.GetKeyCode() == WXK_TAB) do_commit();
else e.Skip();
});
}
void SketchInlineEditor::open(const wxPoint& screen_px, double value,
const std::string& title,
void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel)
{
if (m_frame == nullptr || m_ctrl == nullptr) { if (on_cancel) on_cancel(); return; }
// Where the frame is kept mapped, never close/unmap on the way in: the previous queued
// dimension left it mapped (see do_commit) and re-mapping is what mutter refuses to focus,
// so reuse it and just re-title/re-position. Elsewhere, force a fresh map.
if (!keep_mapped_between_fields && m_frame->IsShown())
m_frame->Hide();
m_anchor = canvas_px;
m_title = title;
m_err.clear();
m_commit = std::move(on_commit);
m_cancel = std::move(on_cancel);
m_ctrl->ChangeValue(en_format(value));
if (m_title) {
m_title_text = wxString::FromUTF8(title.c_str());
m_title->SetLabel(m_title_text);
m_title->SetForegroundColour(kTitleFg); // drop any refusal left over from the last field
m_title->Show(!title.empty());
}
m_frame->Fit();
const wxSize sz = m_frame->GetSize();
wxPoint pos = clamp_to_display(wxPoint(screen_px.x - sz.GetWidth() / 2,
screen_px.y - sz.GetHeight() / 2),
sz, screen_px, m_frame);
// Show() BEFORE Move(): GTK ignores a Move() issued before the window is mapped (the
// WM places it at its default, i.e. the top-left corner). Move after Show sticks.
if (!m_frame->IsShown())
m_frame->Show();
m_frame->Move(pos);
present_toplevel(m_frame); // activate the top-level so SetFocus routes
m_frame->SetFocus();
m_ctrl->SetFocus();
m_ctrl->SelectAll();
m_open = true;
trace_inline_focus(m_frame, title);
// Re-assert on the next tick too: the GL canvas can reclaim focus while it finishes
// handling the click/render that opened us, so a single immediate SetFocus may be stolen.
m_ctrl->CallAfter([this, title] {
if (m_open && m_ctrl) {
present_toplevel(m_frame);
m_ctrl->SetFocus();
m_ctrl->SelectAll();
trace_inline_focus(m_frame, title);
}
});
const std::string v = fmt_value(value);
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
m_open = true;
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
// appears is what makes typing land without a click. There is no window manager to consult.
m_focus_pending = true;
ux_trace("open", m_title, "prefill=" + v);
}
void SketchInlineEditor::do_commit()
void SketchInlineEditor::close()
{
if (!m_open || m_ctrl == nullptr) return;
double v = 0.0;
if (!en_parse(m_ctrl->GetValue(), v)) { // invalid: keep editing, and SAY SO
// Silence here read as a freeze: Enter did nothing, the text re-selected itself, and
// nothing on screen said the value had been refused or what would be accepted. Every
// other CAD names the problem in place; so do we.
flag_invalid(m_ctrl->GetValue().Strip(wxString::both).IsEmpty()
? _L("Enter a number")
: _L("Not a number"));
m_ctrl->SetFocus();
m_ctrl->SelectAll();
return;
}
auto cb = m_commit;
m_open = false; // logically closed; whether it stays MAPPED is per-toolkit
m_commit = nullptr;
m_cancel = nullptr;
// Unmap BEFORE the callback where we are not keeping it mapped, so the reopen the callback
// schedules starts from a hidden frame — the ordering that shipped before the workaround.
if (!keep_mapped_between_fields)
m_frame->Hide();
if (cb) cb(v);
// Kept mapped: the callback either re-opens us for the next queued dimension (via its own
// CallAfter, queued during cb(v), therefore BEFORE the one below) or it does not. Hiding
// here would unmap the window and mutter would refuse to focus the re-map; so hide only
// after the reopen has had its turn. Harmless on the unmapped path — already hidden.
m_frame->CallAfter([this] {
if (m_open || m_frame == nullptr) return;
m_frame->Hide();
return_focus(); // the chain is over; the keyboard belongs to the canvas again
});
m_open = false;
m_focus_pending = false;
m_commit = nullptr;
m_cancel = nullptr;
m_err.clear();
}
void SketchInlineEditor::cancel()
{
if (m_open) do_cancel();
else if (is_mapped()) dismiss(); // orphan: logically gone, still on screen, still eating keys
}
bool SketchInlineEditor::is_mapped() const
{
return m_frame != nullptr && m_frame->IsShown();
}
// Everything the frame can hold onto, released — with no m_open guard, because the state this
// exists for is precisely the one where m_open lies.
void SketchInlineEditor::dismiss()
{
if (m_frame == nullptr) return;
m_open = false;
m_commit = nullptr;
m_cancel = nullptr;
if (m_frame->IsShown()) m_frame->Hide();
return_focus();
}
// Hiding the frame is not enough: X keeps the input focus pointed at the window that had it, so
// an unmapped field keeps swallowing keys. The canvas has to ask for it back explicitly.
void SketchInlineEditor::return_focus()
{
if (m_parent == nullptr) return;
if (wxWindow* top = wxGetTopLevelParent(m_parent))
top->Raise();
m_parent->SetFocus();
}
// Accept what is typed and close. Leaving a tool must not silently discard the value the user
// just entered — the same rule set_tool already follows for a ready edit-op.
void SketchInlineEditor::commit()
{
// Same orphan case as cancel(): Enter or Tab forwarded by the panel must not be the one
// gesture that leaves the field on screen.
if (!m_open) { if (is_mapped()) dismiss(); return; }
do_commit();
// do_commit REFUSES to close on unparseable text, which is right while the user is still
// typing — but this entry point is "we are leaving", and the caller (set_tool) unfreezes
// the canvas immediately afterwards. Refusing here left the field alive and focused over a
// viewport that was interactive again, editing geometry nothing was pointing at any more.
// We cannot accept the text and we must not keep it: fall back to keep-as-drawn, the same
// thing Esc means.
if (m_open) do_cancel();
}
// Re-fit around a changed title, keeping the field itself where it is. The frame is anchored
// top-left, so growing it can push the right edge off the display — re-clamp after the Fit.
void SketchInlineEditor::refit()
void SketchInlineEditor::commit()
{
if (m_frame == nullptr) return;
const wxPoint at = m_frame->GetPosition();
m_frame->Fit();
m_frame->Move(clamp_to_display(at, m_frame->GetSize(), at, m_frame));
}
void SketchInlineEditor::flag_invalid(const wxString& why)
{
if (m_title == nullptr) return;
m_title->SetLabel(why);
m_title->SetForegroundColour(kTitleErr);
m_title->Show(true);
refit(); // "Not a number" is wider than "Length" — without this it renders as "Not a"
m_title->Refresh();
}
void SketchInlineEditor::clear_invalid()
{
if (m_title == nullptr || m_title->GetForegroundColour() != kTitleErr) return;
m_title->SetLabel(m_title_text);
m_title->SetForegroundColour(kTitleFg);
m_title->Show(!m_title_text.IsEmpty());
refit();
m_title->Refresh();
if (m_open) do_commit();
}
void SketchInlineEditor::do_cancel()
{
if (!m_open) return;
ux_trace("cancel", m_title, "");
auto cb = m_cancel;
close();
if (cb) cb();
}
void SketchInlineEditor::close()
void SketchInlineEditor::do_commit()
{
// m_closing was written and never read — a flag that looked like re-entrancy protection
// and was not. Hide() below pumps native events, so a nested close is reachable in
// principle; read the flag and the guard becomes real.
if (m_frame == nullptr || !m_open || m_closing) return;
m_closing = true;
m_open = false;
m_frame->Hide();
m_commit = nullptr;
m_cancel = nullptr;
m_closing = false;
return_focus();
double v = 0.0;
if (!parse_value(m_buf, v)) {
// Refusing input in silence is indistinguishable from the app having frozen: the field
// just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing.
ux_trace("refused", m_title, std::string("typed=") + m_buf);
m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number"));
m_focus_pending = true;
return;
}
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
auto cb = m_commit;
close();
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
// then Height), and doing that into a field that still believes it is open would drop the
// second one's prefill on the floor.
if (cb) cb(v);
}
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
{
if (!m_open) return false;
ImGuiWrapper::push_common_window_style(scale);
imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
// NoInputs is what every other sketch overlay sets and is exactly what this one must not:
// it is the only overlay in the tab that the user types into.
imgui.begin(std::string("##sketchvalue"),
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
if (!m_title.empty() || !m_err.empty()) {
if (m_err.empty()) {
imgui.text(m_title);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f)));
imgui.text(m_err);
ImGui::PopStyleColor();
}
}
if (m_focus_pending) {
ImGui::SetKeyboardFocusHere();
m_focus_pending = false;
}
ImGui::PushItemWidth(90.0f * scale);
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
// wxTextCtrl and is what makes typing a value a single gesture.
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
// processes ImGui's queued characters) versus frames that run while the input is not active —
// and they are indistinguishable from outside.
if (std::getenv("ORCA_CAD_UXTRACE")) {
const ImGuiIO& io = ImGui::GetIO();
std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n",
m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard,
(int) ImGui::IsItemActive(), m_buf);
std::fflush(stderr);
}
ImGui::PopItemWidth();
imgui.end();
ImGui::PopStyleVar();
ImGuiWrapper::pop_common_window_style();
// Keep the frames coming while the field is up — see request_frame's note in the header.
if (m_open && request_frame)
request_frame();
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
do_commit();
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
do_cancel();
return true;
}
}} // namespace Slic3r::GUI
+63 -45
View File
@@ -4,72 +4,90 @@
#include <functional>
#include <string>
#include <wx/window.h>
class wxFrame;
class wxTextCtrl;
class wxStaticText;
class wxPoint;
#include <wx/gdicmn.h>
namespace Slic3r {
namespace GUI {
// Onshape-style in-canvas value editor: a small borderless floating frame holding a
// wxTextCtrl, shown at screen coordinates over the GL canvas. A top-level frame is
// used (not a child widget) because a native child cannot be composited over the
// double-buffered wxGLCanvas under GTK3/llvmpipe — it stays invisible. Enter (or blur)
// commits the parsed number, Esc cancels. This is the single numeric-entry path for
// sketch dimensions, replacing the docked/modal value cards.
class ImGuiWrapper;
// Onshape-style in-canvas value editor.
//
// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and
// that is the whole history of this file: a separate top-level window can only receive typing
// if the window manager grants it focus, and whether it does is not ours to decide. openbox
// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field
// appeared, showed its value selected, and silently ignored every keystroke — Enter then
// committed the number it opened with. Seven workarounds were tried against that (a real X11
// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint,
// keeping the frame mapped between two queued fields, forwarding keys from the panel's
// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the
// field before typing — which is the workaround a user cannot be asked to perform, and is
// exactly the "label value not editable" report.
//
// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the
// same screen point as before, and its keys arrive through the canvas's own key events, which
// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The
// canvas is part of the main window and already has focus, so there is no second window, no
// second focus, and no window manager in the path. The dimension labels next to it are already
// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new
// one.
//
// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame.
class SketchInlineEditor
{
public:
explicit SketchInlineEditor(wxWindow* parent_canvas);
SketchInlineEditor() = default;
// Show the editor centred on `screen_px` (absolute screen coords), pre-filled with
// `value`. on_commit(parsed) fires on Enter with a valid number; on_cancel() on Esc.
void open(const wxPoint& screen_px, double value, const std::string& title,
// Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the
// sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires
// on Enter with a valid number; on_cancel() on Esc.
void open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel);
void close();
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
bool is_open() const { return m_open; }
// MAPPED is not the same question as OPEN, and conflating them is how the keyboard dies.
// The frame is deliberately left mapped across a queued dimension chain (mutter refuses
// focus to a re-mapped window), so there is a window in which m_open is already false and
// the frame is still on screen holding the X input focus. GTK meanwhile reports the window
// inactive, so it routes nothing to the text control — and every key the user presses lands
// in a window that cannot use it and will not give it back. Delete, Esc and typing all read
// as dead. Callers ask this to find the orphan; dismiss() is how they kill it.
bool is_mapped() const;
void dismiss(); // unconditional teardown: works on an ORPHANED frame too
private:
void return_focus(); // hand the keyboard back to the canvas, not to a hidden window
public:
// True when the field itself holds keyboard focus. Callers use this to decide whether the
// field will handle a key on its own or needs it forwarded — see DesignPanel's CHAR_HOOK.
bool has_focus() const { return m_ctrl != nullptr && wxWindow::FindFocus() == m_ctrl; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
//
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
// (nothing further) <- the canvas has nothing to redraw, so it stops
//
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.
bool is_mapped() const { return m_open; }
bool has_focus() const { return m_open; }
void dismiss() { close(); }
private:
void do_commit();
void do_cancel();
// Say WHY a value was refused, in the title line above the field. Refusing input in
// silence is indistinguishable from the app having frozen — the field just sits there
// with the text re-selected and the user has no idea what it wants.
void refit(); // re-Fit around a changed title, then re-clamp on-screen
void flag_invalid(const wxString& why);
void clear_invalid();
wxWindow* m_parent{nullptr}; // the GL canvas: where focus must go back to
wxFrame* m_frame{nullptr};
wxTextCtrl* m_ctrl{nullptr};
wxStaticText* m_title{nullptr};
std::function<void(double)> m_commit;
std::function<void()> m_cancel;
bool m_open{false};
bool m_closing{false};
wxString m_title_text; // the real title, restored after an error message
bool m_open{false};
bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening
wxPoint m_anchor{0, 0}; // canvas device px
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
};
}} // namespace Slic3r::GUI
+17
View File
@@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
if (evt.GetEventType() == wxEVT_CHAR) {
// Char event
const auto key = evt.GetUnicodeKey();
// THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui
// is ever handed a character, so an ImGui text field that stays empty while reporting
// itself active has exactly two possible causes, and this line separates them: no output
// at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of
// ImGui entirely), while output with unicode=0 means the character arrived empty and is
// being dropped right here.
//
// It lives here rather than on the canvas because a probe bound on the canvas CANNOT
// answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx
// runs handlers in reverse bind order, and on_char returns without Skip() whenever this
// function returns true — so such a probe stays silent whether or not the key arrived.
// A day was lost to reading that silence as evidence.
if (std::getenv("ORCA_CAD_UXTRACE")) {
fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n",
(int) key, evt.GetKeyCode(), (int) io.WantTextInput);
fflush(stderr);
}
if (key != 0) {
io.AddInputCharacter(key);
}
+1 -1
View File
@@ -1361,7 +1361,7 @@ void MainFrame::init_tabpanel() {
m_design_page = new wxPanel(this);
m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL));
m_design_page->Hide();
start_mcp_control_if_enabled(); // opens the MCP socket iff SNAPORCA_MCP is set
start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set
}
#endif