diff --git a/src/slic3r/GUI/DesignCanvas.cpp b/src/slic3r/GUI/DesignCanvas.cpp index 3dcdb1eb9b..7c2e02bc4d 100644 --- a/src/slic3r/GUI/DesignCanvas.cpp +++ b/src/slic3r/GUI/DesignCanvas.cpp @@ -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 @@ -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 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; diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp index b0d8c8b97a..f01726fb92 100644 --- a/src/slic3r/GUI/DesignCanvas.hpp +++ b/src/slic3r/GUI/DesignCanvas.hpp @@ -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* 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 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. diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index 32437597de..85a3c88f9e 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -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 action; }; + struct FeatVar { const char* icon; wxString tip; wxString hint; std::function action; int key = 0; }; struct FeatFlyout { std::vector items; // mainline DropDown is Item-based (text/tip/icon per row) std::vector> 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 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 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 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 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 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. diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 154e510f21..63b74cae1d 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #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> m_keys_sketch; + std::map> 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 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 m_body_visible; diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index 7b6c24e0ac..163044fdb4 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -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()); fill.add_vertex((Vec3f)c[1].cast()); + fill.add_vertex((Vec3f)c[2].cast()); fill.add_triangle(fb, fb+1, fb+2); fb += 3; + fill.add_vertex((Vec3f)c[0].cast()); fill.add_vertex((Vec3f)c[2].cast()); + fill.add_vertex((Vec3f)c[3].cast()); 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()); g.add_vertex((Vec3f)(b + off).cast()); + g.add_vertex((Vec3f)(b - off).cast()); g.add_vertex((Vec3f)(a - off).cast()); + 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()); diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index ad0e4169ef..0168f2b8c3 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -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 m_datum_planes; std::vector m_datum_sizes; // per-plane (u,v) full extent; empty -> default GLModel m_solid_face_model;