diff --git a/docs/rig_build_traps.md b/docs/rig_build_traps.md index 365d5f9392..02d1db54a4 100644 --- a/docs/rig_build_traps.md +++ b/docs/rig_build_traps.md @@ -121,3 +121,31 @@ invisible and the session hunts a phantom. Pass `BIN` explicitly: Note also that the GUI containers do **not** mount `scripts/`: `/OrcaSlicer/scripts` inside them is the baked copy, so a local edit to `gui-session.sh` has no effect until you `docker cp scripts/gui-session.sh :/OrcaSlicer/scripts/`. + +--- + +## Trap 6 — `src/Release/` resolves resources to `build/resources`, which may not exist + +The binary derives `resources_dir()` from its own location, so the `src/Release/` one looks in +`/OrcaSlicer/build/resources` while the packaged one looks inside `build/package/`. Only the +packaging step creates the latter; nothing creates the former. Without it the app fails every +`Failed to add custom font ".../build/resources/fonts/…"`, logs `Health check is not running`, +and **exits 255 with nothing on stdout** — which reads exactly like a crash in whatever you just +changed. Measured 2026-08-02: an hour was nearly spent bisecting a GUI change that was fine. + +`build/` is the shared cache volume, so one symlink fixes it permanently, and pointing it at the +bind-mounted repo tree means the rig also picks up new `resources/images/*.svg` without a rebuild: + + docker exec -gui ln -sfn /OrcaSlicer/resources /OrcaSlicer/build/resources + +Tell the two apart before debugging: a resource failure dies in the first second with no window; +a real fault in your code gets past the version banner. Compare +`~/.config//log/.log.0` against a known-good run — 47 lines versus 340 is the tell. + +## Trap 7 — a single-instance app plus a path-matched `pkill` + +`gui-session.sh` used to kill by `"$BIN"`, while its own `app_pid()` matched by BASENAME. Launch +with a `BIN` that differs from the running instance's path and the old process survives, keeps +the single-instance lock, and the new one exits seconds after loading fonts — then `status` +reports the *stale* pid as a healthy session. Fixed by killing on the basename; `status` now also +prints `binary : $(readlink -f /proc//exe)`. **Read that line before trusting a screenshot.** diff --git a/docs/ux/tool_atlas.json b/docs/ux/tool_atlas.json index 1b0a104fef..1d70edf7df 100644 --- a/docs/ux/tool_atlas.json +++ b/docs/ux/tool_atlas.json @@ -567,7 +567,7 @@ "gui": true, "action": "key:S+X", "icon": "design_cut", - "hint": "Split the body with a plane; set the offset in the card" + "hint": "Trim the body with a plane — drag the offset arrow; keep one half or both" }, { "id": "split", @@ -586,7 +586,7 @@ "gui": false, "action": null, "icon": null, - "hint": "Split the body into separate solids" + "hint": "Split the body along a picked face into two solids" }, { "id": "fillet", diff --git a/scripts/gui-session.sh b/scripts/gui-session.sh index 9bf44d076f..6957040b4d 100755 --- a/scripts/gui-session.sh +++ b/scripts/gui-session.sh @@ -53,6 +53,10 @@ status() { else echo "vnc : DOWN"; fi local p; p="$(app_pid || true)" echo "app : ${p:-DOWN}" + # WHICH binary is on screen, not just that something is. A pid alone cannot tell you whether + # you are looking at the build you just linked or one from last week, and that is precisely + # the question every rig verification is asking. + [ -n "${p:-}" ] && echo "binary : $(readlink -f "/proc/$p/exe" 2>/dev/null || echo unknown)" [ -n "${p:-}" ] && echo "windows : $(xdotool search --name . getwindowname %@ 2>/dev/null | paste -sd'|' -)" return 0 } @@ -73,7 +77,15 @@ if ! pgrep -x x11vnc >/dev/null && command -v x11vnc >/dev/null; then fi # --- app ------------------------------------------------------------------------------------ -pkill -9 -f "$BIN" 2>/dev/null || true +# Kill by BASENAME, not by "$BIN". The app enforces a single instance, so an older copy launched +# from a DIFFERENT path (the packaged build/package/bin/ one, say, when BIN points at the freshly +# linked build/src/Release/ one) survives a path-matched pkill, keeps the instance lock, and the +# new process exits seconds after loading fonts — leaving no error anywhere. app_pid() below has +# always matched by basename, so status then reported that stale process as a healthy session: +# the launch looked green while the window on screen was days old. Measured 2026-08-02, where it +# nearly passed a UI change against a Jul-30 binary. The killer and the reporter must agree on +# what counts as "the app". +pkill -9 -f "$(basename "$BIN")" 2>/dev/null || true sleep 2 nohup "$BIN" >"$LOG" 2>&1 & echo "launched $(basename "$BIN") pid $!" diff --git a/src/slic3r/GUI/DesignOffer.hpp b/src/slic3r/GUI/DesignOffer.hpp index 076e236973..bf3cb83910 100644 --- a/src/slic3r/GUI/DesignOffer.hpp +++ b/src/slic3r/GUI/DesignOffer.hpp @@ -99,8 +99,8 @@ static const OfferVerb kOfferVerbs[] = { {"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"}, {"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"}, {"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"}, - {"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x00000480u, 1, 0, false, false, nullptr, "design_cut", "Split the body with a plane; set the offset in the card"}, - {"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x00000080u, 1, 0, false, false, nullptr, nullptr, "Split the body into separate solids"}, + {"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x00000480u, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"}, + {"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x00000080u, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"}, {"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"}, {"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"}, {"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"}, diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index fc28d38dfd..c57cd365fb 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -8,8 +8,10 @@ #include #include +#include // the offer/atlas join check reports on the log #include +#include #include #include #include @@ -477,6 +479,21 @@ DesignPanel::DesignPanel(wxWindow* parent) FeatFlyout() : drop(items) {} }; auto feat_dropdown = [&](const char* id, const char* def_icon, const wxString& grp, std::vector vars) { + // REGISTER FIRST, BUILD SECOND. A verb's two addresses — "fly:#" for the + // offer and its Shift+key — are data. The widget is one door onto them, not their + // owner. Doing this before any wxWindow exists is what lets the build below be skipped + // 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. + // Keyed on "fly:#" 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) { + if (vars[i].key) m_keys_feature[vars[i].key] = vars[i].action; // key runs the same action + m_verb_actions["fly:" + std::string(id) + "#" + std::to_string(i)] = vars[i].action; + } + if (kBarKeep.count(id) == 0) + return; // reached from the offer alone — no button, no chevron, no popup auto* b = icon_btn(def_icon, grp); b->SetFont(Label::Body_14); // measure popup labels in the popup's font (no truncation) auto fo = std::make_shared(); @@ -488,12 +505,6 @@ DesignPanel::DesignPanel(wxWindow* parent) fo->items.push_back(it); fo->actions.push_back(std::move(v.action)); fo->icon_names.emplace_back(v.icon); - if (v.key) m_keys_feature[v.key] = fo->actions.back(); // key runs the same action - // …and the offer reaches the same action by its ratified address. Keyed on - // "fly:#" so the generated table can name it without the item - // struct growing a field at 26 call sites. - m_verb_actions["fly:" + std::string(id) + "#" + - std::to_string(fo->actions.size() - 1)] = fo->actions.back(); } fo->btn = b; fo->drop.Create(b); @@ -524,7 +535,6 @@ DesignPanel::DesignPanel(wxWindow* parent) chev->SetForegroundColour(dp_sec_text()); chev->SetFont(Label::Body_9); fadd(id, chev); - return b; }; auto* b_sketch = icon_btn("design_sketch", _L("Sketch")); @@ -1088,6 +1098,19 @@ DesignPanel::DesignPanel(wxWindow* parent) ToolFlyout() : drop(items) {} }; auto dropdown = [&](const char* def_icon, const wxString& grp, std::vector vars) { + // Register first, build second — the same law as feat_dropdown, for the same reason. + // 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. + for (size_t i = 0; i < vars.size(); ++i) { + const DesignSketchTool::Mode mode = vars[i].mode; + const wxString hint = vars[i].hint; + m_verb_actions["fly:" + std::string(def_icon) + "#" + std::to_string(i)] = + [mode, hint, select_tool] { select_tool(mode, hint); }; + } + if (kBarKeep.count(def_icon) == 0) + return; // the drawing tools live in the offer; nothing of this family is built auto* b = icon_btn(def_icon, grp); // messureSize() measures labels with the PARENT's font (this button) but the // popup draws them in Body_14 — so an under-sized button font truncates rows. @@ -1104,17 +1127,6 @@ DesignPanel::DesignPanel(wxWindow* parent) fo->modes.push_back(v.mode); fo->hints.push_back(v.hint); fo->icon_names.emplace_back(v.icon); - // …and the offer reaches the same tool by its ratified address, exactly as - // feat_dropdown does for the model verbs. Without this 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. - const DesignSketchTool::Mode mode = v.mode; - const wxString hint = v.hint; - m_verb_actions["fly:" + std::string(def_icon) + "#" + - std::to_string(fo->modes.size() - 1)] = - [mode, hint, select_tool] { select_tool(mode, hint); }; } fo->btn = b; fo->drop.Create(b); @@ -1148,7 +1160,6 @@ DesignPanel::DesignPanel(wxWindow* parent) chev->SetForegroundColour(dp_sec_text()); chev->SetFont(Label::Body_9); sadd(chev); // follows its button off the bar - return b; }; skbtn("design_select", DesignSketchTool::Mode::Select, _L("Select"), _L("Click to select; Shift to add; double-click for a whole loop")); @@ -3660,6 +3671,41 @@ DesignPanel::DesignPanel(wxWindow* parent) outer->Add(body, 1, wxEXPAND); SetSizer(outer); + // The atlas says a verb is wired; the registrations above say what it runs. Nothing checks + // that the two agree, and a broken join is INVISIBLE — the row renders, is enabled, and does + // nothing when picked. That defect has shipped three times (edit_feature and sk_move carrying + // action:null, then a whole flyout family the moment its widget stopped being built), and it + // cannot be seen by reading either side alone. Verify the join once, here, where every + // registration is complete. Cheap: 86 map lookups, once per panel. + { + std::vector dead; + for (int i = 0; i < kOfferVerbCount; ++i) { + const OfferVerb& v = kOfferVerbs[i]; + if (v.action == nullptr || *v.action == '\0') + continue; // kernel support, no GUI path — the row shows disabled + const std::string a(v.action); + bool ok = false; + if (a.rfind("key:", 0) == 0) { // resolved exactly as run_offer_action() resolves it + const std::string k = a.substr(4); + if (k.size() >= 3 && k[0] == 'S' && k[1] == '+') { + auto it = m_keys_feature.find(int(k[2]) | SC_SHIFT); + ok = it != m_keys_feature.end() && bool(it->second); + } else if (!k.empty()) { + auto it = m_keys_sketch.find(int(k[0])); + ok = it != m_keys_sketch.end() && bool(it->second); + } + } else { + auto it = m_verb_actions.find(a); + ok = it != m_verb_actions.end() && bool(it->second); + } + if (!ok) + dead.emplace_back(std::string(v.id) + " -> " + a); + } + for (const std::string& d : dead) + BOOST_LOG_TRIVIAL(error) << "Design offer: wired verb has no action: " << d; + assert(dead.empty()); // debug builds stop here; release ships the log line + } + set_ui_mode(UiMode::Feature); }