Hide the Design tab's reference planes until a sketch needs them, with an Origin row to pin them

This commit is contained in:
SoftFever
2026-10-06 18:38:57 +08:00
parent 064eee8b84
commit 78a22fca93
5 changed files with 131 additions and 75 deletions
+13 -2
View File
@@ -181,6 +181,17 @@ canvas (a sibling of the canvas, not a child over it — on GTK a child window o
a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that
changes any of the three belongs with a change to this section.
The reference planes — XY, XZ and YZ through the modeling origin, with their half-axes — are
drawn on demand, because three translucent squares over every model are noise once they are not
the thing being picked. Sketch mode brings them up, since choosing the sketch plane is exactly
when they are picked; a live session draws none, and Finish or Cancel takes them away. The
Feature tree's Origin row, pinned above the features and never removable, keeps them up outside a
sketch. Its state is a view preference in AppConfig rather than part of the recipe, so it costs
the project format nothing. The Plane tool keeps its own rule: the planes and the datums as Offset
bases, and nothing for the other methods, where a click on a plane would rewrite the datum's
references. The `P` and `A` keys are a separate, unpickable view helper and do not follow the
Origin row.
## Rendering the bodies
The tab draws its bodies through the same `GLCanvas3D` object path as Prepare, so how they look
@@ -253,8 +264,8 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
constraint-row buttons are Orca's self-painted `Button`, because a native button cannot take a
hover background on macOS. The Feature tree and Bodies lists are a custom-drawn
`DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit,
Show/hide and Delete on a feature, Move, Show/hide and Delete on a body — and the eye shows
whether that row is hidden.
Show/hide and Delete on a feature, Move, Show/hide and Delete on a body, and only Show/hide on
the Origin row above the features — and the eye shows whether that row is hidden.
- **Plates.** This is the one thing the tab does not follow. The canvas has a bed of its own at
the printer bed's home position, whichever plate Prepare has current, and a new document's
modeling origin is that bed's centre. A bed that followed the current plate would slide out
+14 -10
View File
@@ -480,10 +480,13 @@ def type_into_open_field(value, mark):
def enter_sketch(timeout=180):
"""Open a real sketch on a real plane, and PROVE it with the socket before drawing anything.
THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. Shift+S enters sketch MODE and pops the plane
offer; the offer must be dismissed; and the plane itself is chosen by clicking it in the
viewport BEFORE Shift+S. check-gui-sketching.py has always done all four steps. This ladder
did two of them — Design tab, then Shift+S — and went straight to the tool letters.
THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. The reference planes are hidden until Shift+S
enters sketch MODE, so the plane is clicked AFTER it. Entering with a plane an earlier rung
chose pops the plane offer at once, and a click outside a popup menu only closes it, so the
plane is clicked twice, apart: the second click always lands on the plane. No Escape — with
no offer up, Escape in an empty sketch leaves it. Then a tool letter starts the session.
check-gui-sketching.py does the same steps. This ladder once did two of them — Design tab,
then Shift+S — and went straight to the tool letters.
That intermediate state is the trap. `is_sketching` reads 1, every tool key is accepted and
traced, and not one click draws anything, because there is no plane under them. The ladder
@@ -499,9 +502,9 @@ def enter_sketch(timeout=180):
click(132, 53) # Design tab
time.sleep(2.0)
dismiss_modals()
click(*PLANE_PX) # pick the plane IN THE VIEWPORT — before Shift+S
key("shift+s", 1.0)
key("Escape", 0.5) # entering sketch mode pops the offer; dismiss it
key("shift+s", 1.0) # sketch mode brings the reference planes up
click(*PLANE_PX, pause=0.8) # closes the offer if an earlier rung's plane popped it...
click(*PLANE_PX) # ...so this is the click that picks the plane IN THE VIEWPORT
key("p", 0.6) # any sketch tool starts the session on that plane
if try_call("sketch_describe") is not None:
# NO Escape here. Every rung already opens with one to drop whatever tool the last
@@ -511,14 +514,15 @@ def enter_sketch(timeout=180):
# reported all eleven checks failed with "nothing opened" — the tools were arming
# into an empty Feature-mode document.
return
die("no sketch opened after plane click + Shift+S within "
die("no sketch opened after Shift+S + plane click within "
f"{timeout}s — sketch_describe never answered on {A.sock} (trace {TRACE})")
# tool key, the clicks that draw it, and one distinct value per queued field. The values are
# deliberately nothing like the as-drawn size, so a committed prefill cannot coincide with them.
# Where the plane label sits in the viewport before a sketch is open. Same constant the gesture
# ladder uses; it is a label on the 3D view, not a widget, so it moves only if the camera does.
# Where the plane label sits in the viewport once sketch mode has brought the planes up. Same
# constant the gesture ladder uses; it is a label on the 3D view, not a widget, so it moves only
# if the camera does.
PLANE_PX = (913, 359)
# Every coordinate below stays inside 1000..1400 x 500..760 — the box check-gui-sketching.py's
+12 -8
View File
@@ -229,7 +229,9 @@ CHROME_DY = int(os.environ.get("ORCA_CAD_CHROME_DY", "26"))
DESIGN_TAB = (128, 29 + CHROME_DY)
# Feature-tree rows, measured on the rig at 1920x1080: first row centre, then 23 px apart.
# Feature-tree rows, measured on the rig at 1920x1080: the list's first row is centred at y=215,
# then 23 px apart. That first row is the Origin, pinned above the features and never deleted, so
# the first FEATURE row is the second one: Delete on the Origin does nothing.
# x=300, not the label: a second click ON the label opens the inline rename, and Delete then
# edits the text instead of removing the feature.
#
@@ -237,7 +239,7 @@ DESIGN_TAB = (128, 29 + CHROME_DY)
# unshifted click lands 26 px BELOW the first row -- just past its 23 px height -- so the row is
# never selected, Delete does nothing, and reset_document spends 40 rounds on it before dying
# with "could not empty the feature tree". That names the feature tree, which is not the fault.
TREE_ROW0 = (300, 215 + CHROME_DY)
TREE_ROW0 = (300, 215 + 23 + CHROME_DY)
def go_design():
@@ -266,18 +268,20 @@ def reset_document():
def enter_sketch(tool_key, plane_px=(913, 359)):
"""Enter a sketch the way the design law says: pick the plane in the viewport, then the tool.
Shift+S enters sketch MODE and pops the offer; Escape dismisses it; the tool letter then
starts the session on the plane the click selected. All four steps are real input — nothing
here goes through the socket.
Shift+S enters sketch MODE, which brings the reference planes up; the click picks one; the
tool letter then starts the session on it. Entering with a plane an earlier rung chose pops
the offer at once, and a click outside a popup menu only closes it, so the plane is clicked
twice, apart. No Escape: with no offer up, Escape in an empty sketch leaves it. All of it is
real input — nothing here goes through the socket.
"""
leave_sketch()
click(*plane_px)
key("shift+s", 0.8)
key("Escape", 0.4) # entering sketch mode pops the offer; dismiss it
click(*plane_px, pause=0.8) # closes the offer if an earlier rung's plane popped it...
click(*plane_px) # ...so this is the click that picks the plane
key("p", 0.6)
if try_call("sketch_describe") is None:
shot("/shots/gl-enter-failed.png")
die("no sketch opened after plane click + Shift+S (see /shots/gl-enter-failed.png)")
die("no sketch opened after Shift+S + plane click (see /shots/gl-enter-failed.png)")
calibrate_here() # THIS sketch's own camera map, on THIS sketch's own plane
key(tool_key, 0.6)
+81 -49
View File
@@ -9,6 +9,7 @@
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
#include "libslic3r/AppConfig.hpp" // design_* view preferences
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" // put_other_changes: mark the project dirty outside the undo stack
@@ -284,6 +285,10 @@ static std::string commit_icon(bool bodies)
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
// The Feature tree's first row is the Origin, pinned above the features: feature i is row i + 1.
static constexpr int kFeatureRow0 = 1;
// The feature a Feature tree row shows, or wxNOT_FOUND for the Origin row (and for no row).
static int feature_of_row(int row) { return row >= kFeatureRow0 ? row - kFeatureRow0 : wxNOT_FOUND; }
// A row's eye shows the state the row is in; its tip names what a click does.
static DesignRowList::Action eye_action(bool shown)
@@ -291,6 +296,12 @@ static DesignRowList::Action eye_action(bool shown)
return { RowVisibility, shown ? "design_eye" : "design_eye_off", shown ? _L("Hide") : _L("Show") };
}
// What the Origin row's eye and its menu item do.
static wxString origin_toggle_text(bool shown)
{
return shown ? _L("Hide reference planes and axes") : _L("Show reference planes and axes");
}
// Prepare outlines every numeric field (rounded, #4A4A51 on dark). wxSpinCtrlDouble is a
// native GTK control that cannot draw that, and Orca's own SpinInput is int-only — it would
// silently truncate a 2.5 mm radius. So the double spin keeps its arrows and validation and
@@ -1099,8 +1110,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// is editable now (CadBody::user_name), so the verb opens the editor there.
if (const int b = tree_body_selection(); b >= 0)
m_parts->begin_rename(b);
else if (const int sel = tree_selection(); sel != wxNOT_FOUND)
m_tree->begin_rename(sel);
else if (tree_selection() != wxNOT_FOUND)
m_tree->begin_rename(m_tree->selection());
else
set_status(_L("Select a feature, or a body, first — then rename it")); // not an error
};
@@ -3164,8 +3175,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
FromDIP(SidebarProps::ContentMargin()));
tree_inner->Add(new wxStaticLine(m_tree_box), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
FromDIP(SidebarProps::TitlebarMargin()));
// Sized to its rows, so a short history wastes no block, and scrolling past 9.
m_tree = new DesignRowList(m_tree_box, 9);
// Sized to its rows, so a short history wastes no block, and scrolling past 9 features.
m_tree = new DesignRowList(m_tree_box, 9 + kFeatureRow0);
m_tree->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
@@ -3176,8 +3187,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Picking a feature drops any body selection, so the two lists never both claim to be
// "the target" — but ONLY when this tree has a selection. Each list notifies on every
// change, so clicking a body row runs apply_body_row, whose m_tree->unselect() fires
// THIS handler, which would otherwise clear the body row the user had just clicked.
if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->unselect();
// THIS handler, which would otherwise clear the body row the user had just clicked. The
// Origin row counts: it is a selected row like any other.
if (m_parts && m_tree->selection() != wxNOT_FOUND) m_parts->unselect();
const int sel = tree_selection();
// Likewise a viewport pick, which would be drawn just like the feature's faces. Not while
// a card is open: the card reads that pick.
@@ -3199,12 +3211,17 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
m_tree->on_activate = [this] { on_edit_feature(); };
// The Origin row has nothing to edit.
m_tree->on_activate = [this] {
if (tree_selection() != wxNOT_FOUND) on_edit_feature();
};
// The row's own Edit / Show-hide / Delete, on the row the click selected. The body list is
// cleared here too, not left to on_select, which re-clicking the selected row does not run:
// a body row still selected would be what on_toggle_visibility acts on.
m_tree->on_action = [this](int, int id) {
// a body row still selected would be what on_toggle_visibility acts on. The Origin row's one
// icon is its eye.
m_tree->on_action = [this](int row, int id) {
if (feature_of_row(row) == wxNOT_FOUND) { toggle_origin(); return; }
if (m_parts) m_parts->unselect();
switch (id) {
case RowEdit: on_edit_feature(); break;
@@ -3217,11 +3234,21 @@ DesignPanel::DesignPanel(wxWindow* parent)
// double-click is Edit, and F2 is a function key nothing announces. A user who wants to name
// a sketch tries the row, and the row answers.
m_tree->on_menu = [this](int row, const wxPoint& screen) {
wxMenu menu;
// The Origin row is no feature: it cannot be renamed, edited, moved or deleted, and its
// menu holds exactly what its eye does.
const int feat = feature_of_row(row);
if (feat == wxNOT_FOUND) {
const int id_origin = wxWindow::NewControlId();
menu.Append(id_origin, origin_toggle_text(m_show_origin));
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { toggle_origin(); }, id_origin);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
return;
}
// EVERYTHING A ROW CAN DO, in one place. The row's icons are the quick bar, but the menu
// is the reference: the element you click answers with what applies to it, and a menu
// grows without spending an icon nobody recognises. Split into what the row IS (name,
// contents), where it SITS (order, visibility) and what removes it.
wxMenu menu;
const int id_rename = wxWindow::NewControlId();
const int id_edit = wxWindow::NewControlId();
const int id_up = wxWindow::NewControlId();
@@ -3234,8 +3261,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Scale artwork acts on THIS feature's imported outline, so it belongs to the row and
// is offered only where it means something. It used to hide inside the header's Move
// button, which otherwise moved a body — two different subjects on one icon.
const bool art = row < int(m_doc.features.size()) &&
!m_doc.features[row].imported_regions.empty();
const bool art = feat < int(m_doc.features.size()) &&
!m_doc.features[feat].imported_regions.empty();
if (art) menu.Append(id_art, _L("Scale artwork"));
menu.AppendSeparator();
menu.Append(id_up, _L("Move up"));
@@ -3250,7 +3277,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_toggle_visibility(); }, id_vis);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_delete_feature(); }, id_del);
if (art)
menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { on_transform_imported(row); }, id_art);
menu.Bind(wxEVT_MENU, [this, feat](wxCommandEvent&) { on_transform_imported(feat); }, id_art);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
};
@@ -3259,8 +3286,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// renaming belongs on the row, not in a side-panel field. The list hands the name over after
// its editor's events have finished, so rebuilding the rows here is safe.
m_tree->on_rename = [this](int row, const wxString& name) {
if (row < 0 || row >= int(m_doc.features.size())) return;
m_doc.features[row].name = std::string(name.ToUTF8().data());
const int feat = feature_of_row(row);
if (feat == wxNOT_FOUND || feat >= int(m_doc.features.size())) return;
m_doc.features[feat].name = std::string(name.ToUTF8().data());
refresh_tree(); // which syncs the recipe, so the save path persists the name
};
@@ -4422,7 +4450,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
assert(dead.empty()); // debug builds stop here; release ships the log line
}
m_show_origin = wxGetApp().app_config->get_bool("design_show_origin");
set_ui_mode(UiMode::Feature);
// The Origin row from the first paint: nothing else builds the rows before the first edit,
// project load or theme switch.
refresh_tree();
build_phase("wiring");
BOOST_LOG_TRIVIAL(info) << "Design tab build: total " << build_clock.Time() << " ms";
}
@@ -4516,9 +4548,9 @@ void DesignPanel::set_ui_mode(UiMode m)
m_form->FitInside();
}
update_action_bar(); // Sketch/Constrain modes show the unified ✓/✗; Feature idle hides it
// The origin planes follow the mode: entering Sketch offers them even when a body exists,
// leaving it takes them back. Without this they would only refresh on the next tree
// rebuild, which is not an event that happens when you merely press Sketch.
// The reference planes follow the mode (update_reference_planes). Without this they would
// only refresh on the next tree rebuild, which is not an event that happens when you merely
// press Sketch.
update_reference_planes();
// Say where you are, in words, across the top of the viewport.
@@ -7256,7 +7288,7 @@ wxString DesignPanel::idle_hint() const
{
return m_doc.features.empty()
? _L("Nothing yet — import a STEP or a mesh from the toolbar,\n"
"or click a reference plane and right-click it to start a sketch.")
"or press Sketch and click a reference plane to start a sketch.")
: _L("No solid yet — select a sketch and right-click it to Extrude.");
}
@@ -7284,7 +7316,7 @@ void DesignPanel::on_tab_shown()
if (m_viewport) m_viewport->refresh_bed();
hydrate_from_model();
update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
update_reference_planes(); // entering the Design tab
if (show_clock.Time() > 100) // a slow first show is what users report; the usual one is not news
BOOST_LOG_TRIVIAL(info) << "Design tab shown: bed, project recipe and planes in " << show_clock.Time() << " ms";
m_laid_out = true;
@@ -7515,15 +7547,19 @@ void DesignPanel::refresh_tree()
// Preserve the selected row across the rebuild — set_rows() drops the selection, which made
// every edit/add feel like it "lost" the selection (and broke Edit/Move/Delete on the
// just-touched feature).
const int keep = tree_selection();
// just-touched feature). By row, so a selected Origin row stays selected too.
const int keep = m_tree->selection();
// Datum/reference planes carry no solid; feed them to the viewport so they render as
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
refresh_datum_planes();
update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
update_reference_planes();
std::vector<DesignRowList::Row> rows;
rows.reserve(m_doc.features.size());
rows.reserve(m_doc.features.size() + kFeatureRow0);
// Always first and never removable: the reference planes' own switch, and nothing else.
DesignRowList::Action origin_eye = eye_action(m_show_origin);
origin_eye.tip = origin_toggle_text(m_show_origin);
rows.push_back({ "design_plane", _L("Origin"), {}, dp_item_text(), { origin_eye } });
for (size_t fi = 0; fi < m_doc.features.size(); ++fi) {
const CadFeature& f = m_doc.features[fi];
DesignRowList::Row row;
@@ -7618,7 +7654,7 @@ int DesignPanel::tree_body_selection() const
// if a row was selected.
bool DesignPanel::deselect_rows()
{
const bool any = (m_tree && tree_selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
const bool any = (m_tree && m_tree->selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
if (m_tree) m_tree->unselect();
if (m_parts) m_parts->unselect();
return any;
@@ -7933,13 +7969,13 @@ bool DesignPanel::place_on_face()
int DesignPanel::tree_selection() const
{
return m_tree->selection();
return feature_of_row(m_tree->selection());
}
void DesignPanel::set_tree_selection(int row)
void DesignPanel::set_tree_selection(int feature)
{
if (row >= 0 && row < int(m_tree->GetItemCount()))
m_tree->select(row);
if (feature >= 0 && feature + kFeatureRow0 < int(m_tree->GetItemCount()))
m_tree->select(feature + kFeatureRow0);
}
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
@@ -11042,10 +11078,9 @@ void DesignPanel::update_rib_gizmo()
m_rib_thickness ? m_rib_thickness->GetValue() : 0.0);
}
// Onshape default planes: the XY/XZ/YZ reference planes are persistent, transparent, labelled, and
// larger than the bed — shown as the FALLBACK when there is no object yet. When the Plane tool is
// open they additionally surface existing datums so a base can be picked. Single authority for the
// reference-plane overlay (set/clear_base_pick).
// The XY/XZ/YZ reference planes and their axes. Single authority for the overlay
// (set/clear_base_pick): the Plane tool offers them with the datums as Offset bases; otherwise they
// are up while the Feature tree's Origin row shows them, and while a sketch plane is being chosen.
void DesignPanel::update_reference_planes()
{
if (!m_viewport) return;
@@ -11072,26 +11107,23 @@ void DesignPanel::update_reference_planes()
}
return;
}
// Fallback (Onshape default planes): show the 3 reference planes while there is no SOLID body
// yet — so they persist through the 2D-sketch phase and reappear after a sketch is confirmed
// (a sketch creates no body). They no longer block selection: clicking existing geometry wins,
// a base-plane pick only fires on a click that hit nothing else (see on_mouse fall-through).
// Available while there is no solid yet OR while the user is actually choosing a sketch
// plane. The second half fixes a dead end: delete a sketch on a document that still has a
// body, press Sketch, and act_sketch says "click a face or a reference plane" — with the
// reference planes already taken away, because a body existed. The instruction was
// impossible to follow and there was no way to start a sketch at all short of finding a
// face to click.
//
// Not simply always-on: m_dbp_active both RENDERS and picks, so three translucent planes
// would otherwise float over every finished model. Tying them to Sketch mode shows them
// exactly when they are the thing being chosen, and hides them again on Finish.
if (m_doc.bodies.empty() || m_ui_mode == UiMode::Sketch)
// Sketch mode is where a plane is the thing being picked (act_sketch asks for "a face or a
// reference plane"); a live session draws none (DesignSketchTool::draws_reference_axes).
// They never block selection: clicking existing geometry wins, and a base-plane pick only
// fires on a click that hit nothing else (see on_mouse fall-through).
if (m_show_origin || m_ui_mode == UiMode::Sketch)
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
else
m_viewport->clear_base_pick();
}
void DesignPanel::toggle_origin()
{
m_show_origin = !m_show_origin;
wxGetApp().app_config->set_bool("design_show_origin", m_show_origin);
refresh_tree(); // the Origin row's eye, and update_reference_planes(), which repaints
}
TriangleMesh DesignPanel::ghost_from_bodies(const std::vector<TriangleMesh>& per_body) const
{
TriangleMesh out;
@@ -11693,7 +11725,7 @@ void DesignPanel::close_tool()
m_viewport->clear_datum_gizmo();
m_viewport->set_operand_bodies(-1, -1);
m_viewport->set_highlight_sketches({});
update_reference_planes(); // back to no-tool: show the origin planes if there is no object yet
update_reference_planes(); // back to no-tool
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
+11 -6
View File
@@ -383,7 +383,8 @@ private:
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, Sketch mode, Plane tool
void toggle_origin(); // the Origin row's eye: keep the reference planes up, or not
CadDocument m_doc;
@@ -573,6 +574,10 @@ private:
// 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.
bool m_plane_picked{false};
// The Feature tree's Origin row: keeps the reference planes and their axes up
// (update_reference_planes). A view preference (AppConfig "design_show_origin"), not part of
// the recipe.
bool m_show_origin{false};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
@@ -913,9 +918,9 @@ private:
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
std::function<void()> m_value_cancel; // optional action when the card is cancelled
// Feature tree: one row per feature, in feature order, with a per-type icon and the row's
// own Edit / Show-hide / Delete icons. Callers use row indices via
// tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
// Feature tree: the Origin row, then one row per feature, in feature order, with a per-type
// icon and the row's own Edit / Show-hide / Delete icons. Feature i is row i + 1; callers use
// feature indices via tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
DesignRowList* m_tree{nullptr};
// The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected.
// Finding them replays the history, so they are kept per row and topology generation.
@@ -969,10 +974,10 @@ private:
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_selection() const; // selected feature's index, or wxNOT_FOUND (none, or the Origin row)
int tree_body_selection() const; // selected Parts-list body index, or -1
void refresh_parts(); // rebuild the Bodies list under the feature tree
void set_tree_selection(int row);
void set_tree_selection(int feature);
static const char* tree_icon_for(CadFeatureType t);
wxStaticText* m_status{nullptr};