mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Design tab: keyboard shortcuts, plane/axis toggles, section view
- Keyboard shortcuts (Onshape-style, three scoped layers): feature tools on Shift+letter, 2D sketch tools on single letters (in-sketch), view/nav on single letters (out-of-sketch). Dispatched via the DesignPanel CHAR_HOOK. - View toggles: P = origin planes, A = world axis triad (render_view_helpers). - Section view (non-destructive): a single horizontal clip that hides half the model to inspect inside, showing only the solid remaining half (no ghost). Left-panel "Section View" button or X toggles it; PageUp/PageDown move the plane; "Flip Section" button or F shows the opposite half. Never a body/Cut. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
de16d566b2
commit
1673c2c760
@@ -9,6 +9,7 @@
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "3DScene.hpp"
|
||||
#include "MeshUtils.hpp" // ClippingPlane (section view)
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
@@ -139,6 +140,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(m_canvas_widget, 1, wxEXPAND);
|
||||
SetSizer(sizer);
|
||||
@@ -782,6 +784,53 @@ void DesignCanvas::set_datum_planes(std::vector<SketchPlane> planes, std::vector
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
bool DesignCanvas::toggle_planes()
|
||||
{
|
||||
const bool on = m_sketch_tool.toggle_show_planes();
|
||||
request_repaint();
|
||||
return on;
|
||||
}
|
||||
|
||||
bool DesignCanvas::toggle_axes()
|
||||
{
|
||||
const bool on = m_sketch_tool.toggle_show_axes();
|
||||
request_repaint();
|
||||
return on;
|
||||
}
|
||||
|
||||
void DesignCanvas::set_section_plane(bool on, double z, bool keep_upper)
|
||||
{
|
||||
m_section_on = on;
|
||||
// The kept half must read as a SOLID part, never a see-through ghost: make sure no leftover
|
||||
// preview translucency is applied while the section is on. Guarded — a no-op if already opaque.
|
||||
if (on) { set_body_translucent(false); set_body_hidden(false); }
|
||||
if (m_canvas) {
|
||||
if (on) {
|
||||
// GLCanvas3D turns the two clipping planes into a Z-RANGE: set_z_range(-p0.offset,
|
||||
// p1.offset). keep_upper=false keeps the LOWER half (z in [-1e5, z]); keep_upper=true
|
||||
// keeps the OPPOSITE, UPPER half (z in [z, +1e5]). Only HIDES geometry — no bodies.
|
||||
if (keep_upper) {
|
||||
m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d(0.0, 0.0, 1.0), -z)); // min_z = z
|
||||
m_canvas->set_clipping_plane(1, ClippingPlane(Vec3d(0.0, 0.0, 1.0), 1.0e5)); // max_z = +1e5
|
||||
} else {
|
||||
m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d(0.0, 0.0, 1.0), 1.0e5)); // min_z = -1e5
|
||||
m_canvas->set_clipping_plane(1, ClippingPlane(Vec3d(0.0, 0.0, 1.0), z)); // max_z = z
|
||||
}
|
||||
m_canvas->set_use_clipping_planes(true);
|
||||
} else {
|
||||
m_canvas->set_use_clipping_planes(false);
|
||||
}
|
||||
m_canvas->set_as_dirty();
|
||||
}
|
||||
request_repaint();
|
||||
}
|
||||
|
||||
double DesignCanvas::model_mid_z() const
|
||||
{
|
||||
const BoundingBoxf3 bb = m_model.bounding_box_exact();
|
||||
return bb.defined ? bb.center().z() : 0.0;
|
||||
}
|
||||
|
||||
void DesignCanvas::set_readout(const std::string& text)
|
||||
{
|
||||
if (!m_hud || !m_hud_label || !m_canvas_widget) return;
|
||||
|
||||
@@ -184,6 +184,16 @@ public:
|
||||
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
|
||||
void clear_sketch_selection();
|
||||
|
||||
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
|
||||
// state so the caller can echo it in the status bar.
|
||||
bool toggle_planes();
|
||||
bool toggle_axes();
|
||||
|
||||
// Section views (non-destructive): the panel owns the named "Section View N" list; the canvas
|
||||
// just applies/clears one horizontal clip at a time. model_mid_z() is the default cut height.
|
||||
void set_section_plane(bool on, double z, bool keep_upper = false);
|
||||
double model_mid_z() const;
|
||||
|
||||
// Dimension tool: act on the current sketch selection.
|
||||
DesignSketchTool::DimType sketch_dimension_kind() const;
|
||||
double sketch_dimension_current() const;
|
||||
@@ -257,6 +267,11 @@ private:
|
||||
const std::vector<CadBody>* m_color_bodies{nullptr};
|
||||
|
||||
DesignSketchTool m_sketch_tool;
|
||||
|
||||
// Section view: whether a horizontal clip is currently applied (guards Alt+Wheel). The cut
|
||||
// height and the named-view list live in DesignPanel; the canvas is a dumb applier.
|
||||
bool m_section_on{false};
|
||||
|
||||
std::unique_ptr<SketchInlineEditor> m_inline_editor; // floating in-canvas value editor
|
||||
// Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
|
||||
// active tool's current values (fed by the tool's on_readout). Empty text hides it.
|
||||
|
||||
+166
-20
@@ -234,6 +234,66 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_status->Refresh();
|
||||
};
|
||||
|
||||
// Sketch-tool shortcuts (single letters, active only while a sketch is open). Family tools
|
||||
// bind to their default mode; the other modes stay in the toolbar flyout. Registered here
|
||||
// where select_tool is in scope; the closures run at key-press time (members are live by then).
|
||||
auto sk_key = [this, select_tool](int ch, DesignSketchTool::Mode m, const wxString& h) {
|
||||
m_keys_sketch[ch] = [this, select_tool, m, h] { select_tool(m, h); };
|
||||
};
|
||||
sk_key('L', DesignSketchTool::Mode::Line, _L("Line — click start, then end"));
|
||||
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
|
||||
sk_key('C', DesignSketchTool::Mode::CenterCircle, _L("Circle — click center, then radius"));
|
||||
sk_key('A', DesignSketchTool::Mode::ThreePointArc,_L("Arc — click start, end, then a point"));
|
||||
sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then width"));
|
||||
sk_key('E', DesignSketchTool::Mode::Ellipse, _L("Ellipse — center, major end, minor point"));
|
||||
sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points"));
|
||||
sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place"));
|
||||
sk_key('D', DesignSketchTool::Mode::Dimension, _L("Dimension — click 2 points or an entity"));
|
||||
sk_key('T', DesignSketchTool::Mode::Trim, _L("Trim — click a segment to trim it"));
|
||||
sk_key('X', DesignSketchTool::Mode::Extend, _L("Extend — click a line/arc to extend it"));
|
||||
sk_key('O', DesignSketchTool::Mode::Offset, _L("Offset — pick an entity, drag the distance"));
|
||||
sk_key('M', DesignSketchTool::Mode::Mirror, _L("Mirror — pick axis, then entities"));
|
||||
sk_key('F', DesignSketchTool::Mode::Fillet, _L("Fillet — pick two lines, set the radius"));
|
||||
sk_key('H', DesignSketchTool::Mode::Chamfer, _L("Chamfer — pick two lines, set the distance"));
|
||||
// Polygon needs its side count / circumscribed flag pushed to the tool before it starts.
|
||||
m_keys_sketch['G'] = [this, select_tool] {
|
||||
if (m_viewport) {
|
||||
m_viewport->set_sketch_polygon_sides(m_sides ? m_sides->GetValue() : 6);
|
||||
m_viewport->set_sketch_polygon_circumscribed(m_poly_circ && m_poly_circ->GetValue());
|
||||
}
|
||||
select_tool(DesignSketchTool::Mode::Polygon, _L("Polygon — click center, then a vertex"));
|
||||
};
|
||||
// Constrain (finish the live sketch + enter constrain), and Construction toggle.
|
||||
m_keys_sketch['K'] = [this] { enter_constrain_inline(); };
|
||||
m_keys_sketch['Q'] = [this] {
|
||||
if (m_construction) {
|
||||
m_construction->SetValue(!m_construction->GetValue());
|
||||
if (m_viewport && m_viewport->is_sketching())
|
||||
m_viewport->set_sketch_construction(m_construction->GetValue());
|
||||
}
|
||||
};
|
||||
|
||||
// Shift+letter encoder for the feature-tool shortcuts (registered via FeatVar::key below,
|
||||
// and explicitly for the standalone feature buttons).
|
||||
auto SHIFT = [](int ch) { return ch | SC_SHIFT; };
|
||||
|
||||
// View toggles (single letters, active when no sketch is open): P origin planes, A world
|
||||
// axes, X section view (Alt+Wheel slides the cut). Distinct from Shift+P/Shift+X features.
|
||||
auto status_flag = [this](const wxString& on_msg, const wxString& off_msg, bool on) {
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(on ? on_msg : off_msg);
|
||||
m_status->Refresh();
|
||||
};
|
||||
m_keys_feature['P'] = [this, status_flag] {
|
||||
if (m_viewport) status_flag(_L("Origin planes shown"), _L("Origin planes hidden"),
|
||||
m_viewport->toggle_planes());
|
||||
};
|
||||
m_keys_feature['A'] = [this, status_flag] {
|
||||
if (m_viewport) status_flag(_L("World axes shown"), _L("World axes hidden"),
|
||||
m_viewport->toggle_axes());
|
||||
};
|
||||
m_keys_feature['X'] = [this] { toggle_section_view(); }; // toggle the single section on/off
|
||||
|
||||
// Shared flyout glyph tint (used by BOTH the feature and sketch toolbars). Re-tint each
|
||||
// design_* glyph to the DropDown's resolved TEXT colour so it reads on the popup in either
|
||||
// theme: text_color is 0x363636, which darkModeColorFor() maps to a light tone in dark mode
|
||||
@@ -261,7 +321,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// Onshape-style FEATURE flyouts: same themed-DropDown pattern as the sketch toolbar
|
||||
// (tinted glyphs, Body_14 measure, content-width popup) but each entry runs an
|
||||
// arbitrary action — the existing per-feature handler — instead of selecting a Mode.
|
||||
struct FeatVar { const char* icon; wxString tip; wxString hint; std::function<void()> action; };
|
||||
struct FeatVar { const char* icon; wxString tip; wxString hint; std::function<void()> action; int key = 0; };
|
||||
struct FeatFlyout {
|
||||
std::vector<DropDown::Item> items; // mainline DropDown is Item-based (text/tip/icon per row)
|
||||
std::vector<std::function<void()>> actions;
|
||||
@@ -282,6 +342,7 @@ 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
|
||||
}
|
||||
fo->btn = b;
|
||||
fo->drop.Create(b);
|
||||
@@ -316,13 +377,15 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
};
|
||||
|
||||
auto* b_sketch = icon_btn("design_sketch", _L("Sketch"));
|
||||
b_sketch->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
std::function<void()> act_sketch = [this] {
|
||||
populate_plane_choices(m_draw_plane); // surface datum planes in the picker
|
||||
set_ui_mode(UiMode::Sketch);
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(_L("Pick a plane and a sketch tool, then draw"));
|
||||
m_status->Refresh();
|
||||
});
|
||||
};
|
||||
b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
|
||||
m_keys_feature[SHIFT('S')] = act_sketch;
|
||||
fadd(b_sketch);
|
||||
add_sep(m_tb_feature);
|
||||
// Add material: Extrude / Revolve / Sweep / Loft
|
||||
@@ -347,7 +410,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
return;
|
||||
}
|
||||
open_tool(Tool::Extrude);
|
||||
}},
|
||||
}, SHIFT('E')},
|
||||
{"design_revolve", _L("Revolve"), _L("Revolve a profile about an axis"),
|
||||
[this] {
|
||||
m_revolve_sketch_ref = resolve_extrude_sketch();
|
||||
@@ -358,7 +421,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
return;
|
||||
}
|
||||
open_tool(Tool::Revolve);
|
||||
}},
|
||||
}, SHIFT('R')},
|
||||
{"design_sweep", _L("Sweep"), _L("Sweep a profile along a path"),
|
||||
[this] {
|
||||
m_sweep_profile_ref = resolve_extrude_sketch();
|
||||
@@ -370,7 +433,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
return;
|
||||
}
|
||||
open_tool(Tool::Sweep);
|
||||
}},
|
||||
}, SHIFT('W')},
|
||||
{"design_loft", _L("Loft"), _L("Loft (skin) between two or more profiles"),
|
||||
[this] {
|
||||
// Loft skins 2+ profile sketches; need at least two to be meaningful.
|
||||
@@ -385,11 +448,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
m_loft_refs.clear(); // fresh loft: nothing pre-checked
|
||||
open_tool(Tool::Loft);
|
||||
}},
|
||||
}, SHIFT('L')},
|
||||
});
|
||||
|
||||
auto* b_pattern = icon_btn("design_pattern", _L("Pattern"));
|
||||
b_pattern->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
std::function<void()> act_pattern = [this] {
|
||||
// Pattern replicates an existing body — needs at least one solid.
|
||||
if (m_doc.bodies.empty()) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
@@ -398,19 +461,23 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
return;
|
||||
}
|
||||
open_tool(Tool::Pattern);
|
||||
});
|
||||
};
|
||||
b_pattern->Bind(wxEVT_BUTTON, [act_pattern](wxCommandEvent&) { act_pattern(); });
|
||||
m_keys_feature[SHIFT('N')] = act_pattern;
|
||||
fadd(b_pattern);
|
||||
|
||||
auto* b_plane = icon_btn("design_plane", _L("Plane"));
|
||||
b_plane->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
std::function<void()> act_plane = [this] {
|
||||
populate_plane_choices(m_plane_base); // refresh base list w/ existing datum planes
|
||||
reset_plane_refs(); // fresh datum: no captured face/edge refs
|
||||
open_tool(Tool::Plane);
|
||||
});
|
||||
};
|
||||
b_plane->Bind(wxEVT_BUTTON, [act_plane](wxCommandEvent&) { act_plane(); });
|
||||
m_keys_feature[SHIFT('P')] = act_plane;
|
||||
fadd(b_plane);
|
||||
|
||||
auto* b_boolean = icon_btn("design_boolean", _L("Boolean (combine bodies)"));
|
||||
b_boolean->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
std::function<void()> act_boolean = [this] {
|
||||
// A body-body boolean needs at least two solids to combine.
|
||||
if (m_doc.bodies.size() < 2) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
@@ -420,11 +487,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
populate_body_choices();
|
||||
open_tool(Tool::Boolean);
|
||||
});
|
||||
};
|
||||
b_boolean->Bind(wxEVT_BUTTON, [act_boolean](wxCommandEvent&) { act_boolean(); });
|
||||
m_keys_feature[SHIFT('B')] = act_boolean;
|
||||
fadd(b_boolean);
|
||||
|
||||
auto* b_cut = icon_btn("design_cut", _L("Cut (split a body with a plane)"));
|
||||
b_cut->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
std::function<void()> act_cut = [this] {
|
||||
// A plane cut needs at least one solid to slice.
|
||||
if (m_doc.bodies.empty()) {
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
@@ -435,7 +504,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
populate_plane_choices(m_cut_plane);
|
||||
populate_body_choices();
|
||||
open_tool(Tool::Cut);
|
||||
});
|
||||
};
|
||||
b_cut->Bind(wxEVT_BUTTON, [act_cut](wxCommandEvent&) { act_cut(); });
|
||||
m_keys_feature[SHIFT('X')] = act_cut;
|
||||
fadd(b_cut);
|
||||
|
||||
// Color — override the selected body's display colour (per-body, survives recompute).
|
||||
@@ -446,11 +517,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// Dress-up: Fillet/Chamfer / Draft / Shell
|
||||
feat_dropdown("design_dressup", _L("Dress-up (fillet / chamfer / draft / shell)"), {
|
||||
{"design_dressup", _L("Fillet / Chamfer"), _L("Round or bevel a picked edge"),
|
||||
[this] { open_tool(Tool::Dressup); }},
|
||||
[this] { open_tool(Tool::Dressup); }, SHIFT('F')},
|
||||
{"design_draft", _L("Draft (taper a face)"), _L("Tilt a picked face by a draft angle"),
|
||||
[this] { open_tool(Tool::Draft); }},
|
||||
[this] { open_tool(Tool::Draft); }, SHIFT('D')},
|
||||
{"design_shell", _L("Shell"), _L("Hollow the body to a wall thickness, opening a picked face"),
|
||||
[this] { open_tool(Tool::Shell); }},
|
||||
[this] { open_tool(Tool::Shell); }, SHIFT('K')},
|
||||
});
|
||||
|
||||
// Hole / Thread — drilling into a solid (both face-aware)
|
||||
@@ -484,7 +555,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
}
|
||||
open_tool(Tool::Hole);
|
||||
}},
|
||||
}, SHIFT('H')},
|
||||
{"design_thread", _L("Thread"), _L("Thread a cylindrical surface (inner bore / outer) or a circular edge"),
|
||||
[this] {
|
||||
// Driven by a picked CYLINDRICAL surface (inner bore = internal, outer = external)
|
||||
@@ -521,7 +592,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_status->Refresh();
|
||||
}
|
||||
open_tool(Tool::Thread);
|
||||
}},
|
||||
}, SHIFT('T')},
|
||||
});
|
||||
add_sep(m_tb_feature);
|
||||
// Text / SVG insert tools live in the SKETCH toolbar (they produce 2D profiles =
|
||||
@@ -529,6 +600,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
// Import STEP — standalone: a STEP comes in as a whole editable B-rep body, not a profile.
|
||||
auto* b_step = icon_btn("design_step", _L("Import STEP (editable B-rep solid)"));
|
||||
b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); });
|
||||
m_keys_feature[SHIFT('I')] = [this] { on_import_step(); };
|
||||
fadd(b_step);
|
||||
add_sep(m_tb_feature);
|
||||
auto* b_constrain = icon_btn("design_constrain", _L("Constrain selected sketch"));
|
||||
@@ -1520,6 +1592,21 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
}
|
||||
root->Add(m_dof_status, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12);
|
||||
|
||||
// Section View — clear text button (non-destructive: hides part of the model to inspect
|
||||
// inside; adds a named "Section View N", never a body). Distinct from the Cut tool.
|
||||
auto* section_btn = new wxButton(m_form, wxID_ANY, _L("Section View"));
|
||||
section_btn->SetToolTip(_L("Hide part of the model to see inside (non-destructive). "
|
||||
"PageUp/PageDown move the plane; Delete removes it."));
|
||||
section_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { toggle_section_view(); });
|
||||
root->Add(section_btn, 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
|
||||
// Flip the active section to the opposite half — only usable while a section view is active.
|
||||
m_section_flip_btn = new wxButton(m_form, wxID_ANY, _L("Flip Section"));
|
||||
m_section_flip_btn->SetToolTip(_L("Show the opposite half of the active section view"));
|
||||
m_section_flip_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { flip_section_view(); });
|
||||
m_section_flip_btn->Enable(false);
|
||||
root->Add(m_section_flip_btn, 0, wxLEFT | wxRIGHT | wxTOP, 6);
|
||||
|
||||
auto* new_design = new wxButton(m_form, wxID_ANY, _L("New Design"));
|
||||
new_design->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_new_design(); });
|
||||
root->Add(new_design, 0, wxLEFT | wxRIGHT | wxTOP, 12);
|
||||
@@ -1987,6 +2074,30 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
if (m_ui_mode == UiMode::Feature && m_active == Tool::None
|
||||
&& tree_selection() != wxNOT_FOUND) { on_delete_feature(); return; }
|
||||
}
|
||||
// Section view controls while it is on (Alt+Wheel is unreliable under remote desktops / is
|
||||
// grabbed by GLCanvas3D, so the keyboard drives it): PageUp/PageDown move the plane, F flips
|
||||
// which half is kept (so you can inspect the opposite part).
|
||||
if (!in_text && !sketching && m_section_on) {
|
||||
if (key == WXK_PAGEUP || key == WXK_PAGEDOWN) {
|
||||
m_section_cut_z += (key == WXK_PAGEUP ? 2.0 : -2.0);
|
||||
if (m_viewport) m_viewport->set_section_plane(true, m_section_cut_z, m_section_upper);
|
||||
return;
|
||||
}
|
||||
if (key == 'F' || key == 'f') { flip_section_view(); return; }
|
||||
}
|
||||
// Tool shortcuts (Onshape-style). While a sketch is open, single letters drive sketch
|
||||
// tools; otherwise Shift+letter drives feature tools and single letters drive view
|
||||
// toggles / section. Ctrl-combos and focused text fields are never intercepted.
|
||||
if (!in_text && !ctrl) {
|
||||
const int up = (key >= 'a' && key <= 'z') ? key - 'a' + 'A' : key; // normalise case
|
||||
if (sketching) {
|
||||
auto it = m_keys_sketch.find(up);
|
||||
if (it != m_keys_sketch.end()) { it->second(); return; }
|
||||
} else {
|
||||
auto it = m_keys_feature.find(up | (e.ShiftDown() ? SC_SHIFT : 0));
|
||||
if (it != m_keys_feature.end()) { it->second(); return; }
|
||||
}
|
||||
}
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
@@ -3010,6 +3121,41 @@ int DesignPanel::tree_body_selection() const
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DesignPanel::update_section_flip_btn()
|
||||
{
|
||||
if (m_section_flip_btn) m_section_flip_btn->Enable(m_section_on);
|
||||
}
|
||||
|
||||
void DesignPanel::toggle_section_view()
|
||||
{
|
||||
if (!m_viewport) return;
|
||||
m_section_on = !m_section_on;
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
if (m_section_on) {
|
||||
m_section_cut_z = m_viewport->model_mid_z(); // start at the model's mid-height
|
||||
m_section_upper = false; // keep the lower half by default
|
||||
m_viewport->set_section_plane(true, m_section_cut_z, m_section_upper);
|
||||
m_status->SetLabel(_L("Section view on — hides half the model to see inside; "
|
||||
"PageUp / PageDown move the plane, Flip shows the other half"));
|
||||
} else {
|
||||
m_viewport->set_section_plane(false, 0.0);
|
||||
m_status->SetLabel(_L("Section view off"));
|
||||
}
|
||||
m_status->Refresh();
|
||||
update_section_flip_btn();
|
||||
}
|
||||
|
||||
void DesignPanel::flip_section_view()
|
||||
{
|
||||
if (!m_viewport || !m_section_on) return;
|
||||
m_section_upper = !m_section_upper;
|
||||
m_viewport->set_section_plane(true, m_section_cut_z, m_section_upper);
|
||||
m_status->SetForegroundColour(wxNullColour);
|
||||
m_status->SetLabel(wxString::Format(_L("Section view — showing the %s half"),
|
||||
m_section_upper ? _L("upper") : _L("lower")));
|
||||
m_status->Refresh();
|
||||
}
|
||||
|
||||
void DesignPanel::sync_body_visible()
|
||||
{
|
||||
// Keep the visibility vector parallel to bodies; newly-created bodies default visible.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
#include "libslic3r/CadDocument.hpp"
|
||||
|
||||
@@ -195,6 +196,15 @@ private:
|
||||
CadDocument m_doc;
|
||||
|
||||
Tool m_active{Tool::None};
|
||||
|
||||
// Keyboard shortcuts (Onshape-style, three scoped layers). Keys are encoded as the
|
||||
// upper-cased letter, OR'd with 0x10000 when Shift is required. m_keys_sketch fires only
|
||||
// while a sketch is open (single letters = sketch tools); m_keys_feature fires only when
|
||||
// no sketch is open (Shift+letter = feature tools; single letters = view toggles/section).
|
||||
static constexpr int SC_SHIFT = 0x10000;
|
||||
std::map<int, std::function<void()>> m_keys_sketch;
|
||||
std::map<int, std::function<void()>> m_keys_feature;
|
||||
|
||||
wxSizer* m_box_sketch{nullptr};
|
||||
wxSizer* m_box_extrude{nullptr};
|
||||
wxSizer* m_box_dressup{nullptr};
|
||||
@@ -416,6 +426,20 @@ private:
|
||||
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
|
||||
// highlights that body and makes it the target for the next op.
|
||||
std::vector<wxTreeItemId> m_tree_body_items;
|
||||
|
||||
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
|
||||
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
|
||||
// clicking a row activates it (again = off); Delete removes it; Alt+Wheel moves the active one.
|
||||
// Section view (single, non-destructive): ONE horizontal clip that hides half the model to
|
||||
// inspect inside — solid, no ghost of the hidden half. Toggled on/off; Flip shows the other
|
||||
// half. Never a body, no tree entry.
|
||||
bool m_section_on{false};
|
||||
double m_section_cut_z{0.0};
|
||||
bool m_section_upper{false}; // false = keep lower half, true = upper
|
||||
wxButton* m_section_flip_btn{nullptr}; // enabled only while the section is on
|
||||
void toggle_section_view(); // Section View button / X: on <-> off
|
||||
void flip_section_view(); // Flip button / F: opposite half
|
||||
void update_section_flip_btn(); // enable the Flip button iff the section is on
|
||||
// Per-body visibility (parallel to m_doc.bodies; index stable across recompute since
|
||||
// bodies are appended in feature order). Empty/grown to all-visible by sync_body_visible().
|
||||
std::vector<bool> m_body_visible;
|
||||
|
||||
@@ -2887,6 +2887,64 @@ void DesignSketchTool::render_solid_highlight()
|
||||
// rectangle + border so it is visible in the viewport (Onshape-style finite plane). World
|
||||
// space, depth-test off so it reads over the bed; indigo to stay distinct from the cyan
|
||||
// solid-selection tint, orange sketches and amber feature ghosts.
|
||||
void DesignSketchTool::render_view_helpers()
|
||||
{
|
||||
if (!m_show_planes && !m_show_axes) return;
|
||||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||||
using EVL = GLModel::Geometry::EVertexLayout;
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const Vec3d vd = cam.get_dir_forward();
|
||||
const double hw = 1.5 / std::max(cam.get_zoom(), 1e-6); // billboard ribbon half-width (px)
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
|
||||
if (m_show_planes) {
|
||||
const double H = 40.0; // half-extent (mm)
|
||||
SketchPlane pl[3]; // XY / XZ / YZ through the world origin
|
||||
pl[0].origin = Vec3d(0,0,0); pl[0].x_axis = Vec3d(1,0,0); pl[0].y_axis = Vec3d(0,1,0); pl[0].normal = Vec3d(0,0,1);
|
||||
pl[1].origin = Vec3d(0,0,0); pl[1].x_axis = Vec3d(1,0,0); pl[1].y_axis = Vec3d(0,0,1); pl[1].normal = Vec3d(0,1,0);
|
||||
pl[2].origin = Vec3d(0,0,0); pl[2].x_axis = Vec3d(0,1,0); pl[2].y_axis = Vec3d(0,0,1); pl[2].normal = Vec3d(1,0,0);
|
||||
GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 };
|
||||
unsigned int fb = 0;
|
||||
for (const SketchPlane& p : pl) {
|
||||
const Vec3d c[4] = { p.to_world(Vec2d(-H,-H)), p.to_world(Vec2d(H,-H)),
|
||||
p.to_world(Vec2d(H,H)), p.to_world(Vec2d(-H,H)) };
|
||||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[1].cast<float>());
|
||||
fill.add_vertex((Vec3f)c[2].cast<float>()); fill.add_triangle(fb, fb+1, fb+2); fb += 3;
|
||||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[2].cast<float>());
|
||||
fill.add_vertex((Vec3f)c[3].cast<float>()); fill.add_triangle(fb, fb+1, fb+2); fb += 3;
|
||||
}
|
||||
if (fb > 0) { GLModel fm; fm.init_from(std::move(fill));
|
||||
fm.set_color(ColorRGBA(0.42f, 0.52f, 0.78f, 0.20f)); fm.render(); }
|
||||
}
|
||||
|
||||
if (m_show_axes) {
|
||||
const double L = 60.0; // axis length (mm)
|
||||
struct Ax { Vec3d dir; ColorRGBA col; };
|
||||
const Ax axes[3] = { { Vec3d(1,0,0), ColorRGBA(0.92f, 0.28f, 0.28f, 0.9f) },
|
||||
{ Vec3d(0,1,0), ColorRGBA(0.30f, 0.80f, 0.34f, 0.9f) },
|
||||
{ Vec3d(0,0,1), ColorRGBA(0.32f, 0.55f, 0.95f, 0.9f) } };
|
||||
for (const Ax& ax : axes) {
|
||||
// Lines don't rasterise under the reused software GL context, so each axis is a
|
||||
// thin view-facing ribbon (two triangles), like the datum-plane border.
|
||||
const Vec3d a = Vec3d(0,0,0), b = ax.dir * L;
|
||||
Vec3d dir = (b - a).normalized();
|
||||
Vec3d off = dir.cross(vd);
|
||||
if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up());
|
||||
if (off.norm() < 1e-9) continue;
|
||||
off.normalize(); off *= hw * 1.5;
|
||||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||||
g.add_vertex((Vec3f)(a + off).cast<float>()); g.add_vertex((Vec3f)(b + off).cast<float>());
|
||||
g.add_vertex((Vec3f)(b - off).cast<float>()); g.add_vertex((Vec3f)(a - off).cast<float>());
|
||||
g.add_triangle(0, 1, 2); g.add_triangle(0, 2, 3);
|
||||
GLModel m; m.init_from(std::move(g)); m.set_color(ax.col); m.render();
|
||||
}
|
||||
}
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
void DesignSketchTool::render_datum_planes()
|
||||
{
|
||||
if (m_datum_planes.empty()) return;
|
||||
@@ -6541,6 +6599,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
|
||||
if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD
|
||||
return;
|
||||
}
|
||||
render_view_helpers(); // origin planes / world axes — drawn whenever their toggle is on
|
||||
if (m_active && m_mode != Mode::Constrain && m_entities.empty() && m_points.empty()
|
||||
&& m_display_sketches.empty()) {
|
||||
if (on_readout) on_readout(std::string());
|
||||
|
||||
@@ -98,10 +98,18 @@ public:
|
||||
bool has_display() const { return m_active || !m_display_sketches.empty()
|
||||
|| (m_solid_bodies != nullptr && !m_solid_bodies->empty())
|
||||
|| !m_datum_planes.empty()
|
||||
|| m_show_planes || m_show_axes
|
||||
|| m_ex_active || m_mv_active || m_fl_active
|
||||
|| m_hl_active || m_th_active || m_sh_active
|
||||
|| m_dr_active || m_ct_active || m_dz_active || m_dbp_active; }
|
||||
|
||||
// View helpers: the 3 world origin planes (XY/XZ/YZ) and the world axis triad, each
|
||||
// shown/hidden by a toggle (keys P / A). Off by default so the idle scene stays clean.
|
||||
void set_show_planes(bool s) { m_show_planes = s; }
|
||||
void set_show_axes(bool s) { m_show_axes = s; }
|
||||
bool toggle_show_planes() { m_show_planes = !m_show_planes; return m_show_planes; }
|
||||
bool toggle_show_axes() { m_show_axes = !m_show_axes; return m_show_axes; }
|
||||
|
||||
// Solid topology selection on the committed bodies: clicking a solid cycles
|
||||
// whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With
|
||||
// multiple bodies the pick resolves WHICH body was hit (per-triangle body id).
|
||||
@@ -891,6 +899,9 @@ private:
|
||||
bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // cycle + notify
|
||||
void render_solid_highlight();
|
||||
void render_datum_planes(); // translucent rectangles for datum/reference planes
|
||||
void render_view_helpers(); // world origin planes + axis triad (P / A toggles)
|
||||
bool m_show_planes{false};
|
||||
bool m_show_axes{false};
|
||||
std::vector<SketchPlane> m_datum_planes;
|
||||
std::vector<Vec2d> m_datum_sizes; // per-plane (u,v) full extent; empty -> default
|
||||
GLModel m_solid_face_model;
|
||||
|
||||
Reference in New Issue
Block a user